Skip to content

fix: accept whitespace between code fence and yaml info string - #2622

Merged
IsmaelMartinez merged 4 commits into
The-PR-Agent:mainfrom
bunnysayzz:fix/load-yaml-space-info-string-2609
Aug 26, 2026
Merged

IsmaelMartinez merged 4 commits into
The-PR-Agent:mainfrom
bunnysayzz:fix/load-yaml-space-info-string-2609

Conversation

@bunnysayzz

Copy link
Copy Markdown
Contributor

Description

Fixes #2609. load_yaml stripped a yaml prefix but not the CommonMark-valid yaml variant (space between fence and info string), and the snippet-extraction fallback regex also required the info string flush against the fence. A valid fenced YAML response with the spaced form fell through every fallback and returned None, silently dropping the model's structured output.

Two changes in pr_agent/algo/utils.py:

  • load_yaml now strips the spaced fence variants (yaml and yml) alongside the flush forms.
  • The snippet-extraction fallback regex now allows whitespace between the opening fence and the info string, so the extracted body no longer carries the stray info string.

Verification

  • The repro from the issue is included as test_space_before_yaml_info_string (also covers the ``` yml variant). It fails on the original code (returns None) and passes with the fix.
  • Full tests/unittest/test_load_yaml.py suite: 4/4 pass.

Confirmed by maintainer on the issue thread. Thanks @IsmaelMartinez for the review guidance.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Fix load_yaml parsing for spaced Markdown YAML fences

🐞 Bug fix 🧪 Tests 🕐 10-20 Minutes

Grey Divider

AI Description

• Accept CommonMark-valid fenced YAML blocks with whitespace after ```.
• Improve fallback snippet extraction to ignore whitespace before yaml/yml info strings.
• Add unit test covering `` yaml and `` yml variants to prevent regressions.
Diagram

graph TD
  A["LLM response text"] --> B["load_yaml()\nutils.py"] --> C{"yaml.safe_load\nsucceeds?"}
  C -- "yes" --> D["Parsed YAML dict/list"]
  C -- "no" --> E["try_fix_yaml()\nutils.py"] --> F["Snippet regex\n'''[ \t]*(yaml|yml)"] --> G["yaml.safe_load\n(extracted body)"] --> D
  H["Unit tests\ntest_load_yaml.py"] --> B
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a Markdown/CommonMark parser to extract fenced blocks
  • ➕ More robust handling of Markdown edge cases (indentation, multiple fences, language tags)
  • ➕ Avoids regex/string-prefix corner cases over time
  • ➖ Adds dependency and complexity for a small parsing need
  • ➖ Still must decide which fenced block to prefer when multiple exist
2. Normalize fences via a single regex pre-pass (strip + extract)
  • ➕ Centralizes all fence variants (yaml, yaml, case variants) in one place
  • ➕ Reduces reliance on multiple removeprefix calls
  • ➖ Requires careful regex design/testing to avoid removing valid YAML content
  • ➖ Bigger refactor than needed for the reported bug

Recommendation: Keep the current targeted fix: it addresses the specific CommonMark-valid whitespace variant with minimal risk and adds a regression test. Consider a Markdown parser only if additional Markdown-extraction bugs keep recurring.

Files changed (2) +12 / -2

Bug fix (1) +2 / -2
utils.pyHandle spaced ''' yaml/''' yml fences in load_yaml and snippet fallback +2/-2

Handle spaced ''' yaml/''' yml fences in load_yaml and snippet fallback

• Extends load_yaml prefix stripping to accept yml plus fenced variants where the info string is separated by whitespace (e.g., ''' yaml). Updates the fallback snippet-extraction regex to allow spaces/tabs after the opening fence so the captured snippet body excludes the language identifier reliably.

pr_agent/algo/utils.py

Tests (1) +10 / -0
test_load_yaml.pyAdd regression test for whitespace before YAML info string +10/-0

Add regression test for whitespace before YAML info string

• Adds a unit test asserting that fenced YAML parses identically for '''yaml, ''' yaml, and ''' yml forms, covering the CommonMark-allowed whitespace after the opening fence.

tests/unittest/test_load_yaml.py

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0) 📜 Skill insights (0)

Grey Divider


Action required

1. snippet.group(2) wrong capture ✓ Resolved 📎 Requirement gap ≡ Correctness
Description
try_fix_yaml() updated snippet_pattern to use non-capturing groups so it now has only a single
capturing group for the YAML body, but the code still reads snippet.group(2), which will raise an
IndexError when the fenced-snippet fallback is used and break YAML recovery. This turns an
otherwise recoverable YAML-parse failure into a hard exception in load_yaml()/try_fix_yaml()
instead of returning parsed YAML or None.
Code

pr_agent/algo/utils.py[819]

+    snippet_pattern = r'```[ \t]*(?:yaml|yml)?[ \t]*(?:\r?\n|$)([\s\S]*?)```(?=\s*$|")'
Evidence
PR Compliance ID 6 requires the fenced-snippet fallback in try_fix_yaml to correctly extract only
the YAML body from CommonMark-style fenced blocks like ``yaml/``yml (including spaced info-string
variants). The cited code shows snippet_pattern was changed so the info-string portion is
non-capturing and the body is the only capturing group (([\s\S]*?)), yet the subsequent extraction
still calls snippet.group(2), which cannot exist for this pattern and will therefore raise at
runtime whenever the fallback matches. Because this path is exercised by tests expecting fenced YAML
snippet extraction and load_yaml is used by core tools, the resulting exception propagates broadly
and violates robustness/error-handling expectations (PR Compliance ID 3).

load_yaml must accept fenced YAML blocks with optional whitespace before yaml/yml info string
Rule 3: Robust Error Handling
pr_agent/algo/utils.py[819-826]
pr_agent/algo/utils.py[818-832]
tests/unittest/test_try_fix_yaml.py[20-49]
pr_agent/tools/pr_description.py[20-27]
pr_agent/tools/pr_code_suggestions.py[20-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`try_fix_yaml()` changed `snippet_pattern` to use non-capturing groups for the fenced-block info string, leaving only one capturing group for the YAML body, but the extraction logic still calls `snippet.group(2)`. This will raise `IndexError` whenever the fenced-snippet fallback matches, turning YAML recovery from a recoverable parse failure into a hard crash.
## Issue Context
The fenced-snippet fallback is required to work for CommonMark-valid fenced YAML blocks (including the spaced info-string variant) and should extract only the YAML body for ```yaml/```yml fences. Previously the pattern had two capturing groups—`(yaml|yml)?` and the body—but after the update the info-string is non-capturing and only the body is captured, so the group index assumptions must be updated to keep `load_yaml()`/`try_fix_yaml()` robust and returning parsed YAML or `None` rather than throwing.
## Fix Focus Areas
- pr_agent/algo/utils.py[819-826]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. snippet_pattern matches non-YAML fences ✓ Resolved 📎 Requirement gap ≡ Correctness
Description
try_fix_yaml()’s fallback regex still matches fenced blocks with non-yaml/yml info strings
(e.g., ```python), attempting to parse them as YAML. This violates the requirement to limit matching
to bare fences or yaml/yml-labeled fences only and can cause incorrect parsing behavior.
Code

pr_agent/algo/utils.py[819]

+    snippet_pattern = r'```[ \t]*(yaml|yml)?([\s\S]*?)```(?=\s*$|")'
Evidence
PR Compliance ID 6 requires the fenced-snippet fallback to remain limited to bare fences or fences
labeled yaml/yml. The updated regex at pr_agent/algo/utils.py:819 keeps (yaml|yml)? optional
and does not require a newline immediately after the optional yaml/yml identifier, so non-YAML
info strings can still be captured in the snippet body and passed to yaml.safe_load.

load_yaml must parse fenced YAML blocks with optional whitespace before yaml/yml info string
pr_agent/algo/utils.py[818-832]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`try_fix_yaml()` uses `snippet_pattern = r'```[ \t]*(yaml|yml)?([\s\S]*?)```(?=\s*$|")'`, which still matches fenced blocks with arbitrary info strings (e.g., ` ```python\n...``` `) because `(yaml|yml)?` is optional; the info string becomes part of the captured body and is fed into `yaml.safe_load`.
## Issue Context
Compliance requires this fallback to match only:
- bare fences (` ```\n...``` `)
- fences labeled `yaml`/`yml` (including CommonMark-valid whitespace like ` ``` yaml`)
…and **not** broaden to other languages.
## Fix Focus Areas
- pr_agent/algo/utils.py[818-832]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. load_yaml line exceeds 120 📘 Rule violation ⚙ Maintainability
Description
The modified load_yaml() prefix-stripping line is far longer than the configured Ruff `line-length
= 120`, reducing readability and risking lint/pre-commit failures. It should be wrapped or
refactored (e.g., iterate over a list of prefixes) to comply with the repository style rules.
Code

pr_agent/algo/utils.py[755]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Evidence
The repository config sets Ruff line-length = 120 in pyproject.toml. The changed load_yaml()
assignment at pr_agent/algo/utils.py:755 is a single very long chained call sequence that exceeds
this limit, violating the style requirement.

AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments
pyproject.toml[52-54]
pr_agent/algo/utils.py[753-756]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The updated `response_text = ...removeprefix(...).removeprefix(...)...` chain in `load_yaml()` exceeds the Ruff line-length limit (120).
## Issue Context
`pyproject.toml` sets Ruff `line-length = 120`, and compliance requires adhering to Ruff style.
## Fix Focus Areas
- pr_agent/algo/utils.py[753-756]
- pyproject.toml[52-54]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Overbroad yml stripping 🐞 Bug ≡ Correctness
Description
load_yaml now unconditionally applies removeprefix('yml'), which will corrupt valid unfenced
YAML that begins with the key yml (e.g., yml: 1) and can cause parsing to fail and return
None. The fallbacks in try_fix_yaml mainly operate on the already-modified text, and the
unmodified original is only used for the fenced-snippet extraction path.
Code

pr_agent/algo/utils.py[755]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Evidence
The PR change adds removeprefix('yml') to load_yaml’s unconditional prefix stripping. Because
load_yaml is expected to accept plain YAML strings (per existing tests), a plain YAML document
starting with the key yml is a valid input that will now be mutated before parsing. In the failure
path, try_fix_yaml primarily attempts to parse variants of the modified response_text; the only
use of response_text_original in the shown section is for fenced-snippet extraction, which won’t
help for unfenced inputs.

pr_agent/algo/utils.py[753-768]
pr_agent/algo/utils.py[818-832]
tests/unittest/test_load_yaml.py[11-17]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`load_yaml()` strips the literal prefix `yml` from *any* input string. This breaks valid YAML documents that legitimately start with the key `yml` (e.g., `yml: 1`) by turning them into invalid YAML (e.g., `: 1`), which can lead to `load_yaml()` returning `None`.
## Issue Context
`load_yaml()` is used to parse both fenced and unfenced YAML responses across multiple tools; tests demonstrate it is expected to parse plain YAML strings.
## Fix Focus Areas
- pr_agent/algo/utils.py[753-768]
- pr_agent/algo/utils.py[818-832]
- tests/unittest/test_load_yaml.py[11-17]
## Suggested fix approach
- Replace `.removeprefix('yml')` (and ideally the existing `.removeprefix('yaml')`) with a delimiter-aware removal:
- Only strip when the input starts with a standalone marker line like `yml\n` / `yaml\n` (optionally with surrounding whitespace), not when immediately followed by `:` or other non-whitespace.
- Alternatively, parse/remove the first line only if it matches `^(yaml|yml)\s*$`.
- Add a regression test such as `assert load_yaml('yml: 1') == {'yml': 1}` to prevent reintroducing this input corruption.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used

Grey Divider

  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.
Tip of the day
💡 Did you know, you can group findings by type and pick your Finding display, from Minimal to Full

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous review results

Review updated until commit 9711c2a ⚖️ Balanced

Results up to commit baf51f4


🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (1) 📜 Skill insights (0)


Action required
1. snippet.group(2) wrong capture 📎 Requirement gap ≡ Correctness ⭐ New
Description
try_fix_yaml() updated snippet_pattern to use non-capturing groups so it now has only a single
capturing group for the YAML body, but the code still reads snippet.group(2), which will raise an
IndexError when the fenced-snippet fallback is used and break YAML recovery. This turns an
otherwise recoverable YAML-parse failure into a hard exception in load_yaml()/try_fix_yaml()
instead of returning parsed YAML or None.
Code

pr_agent/algo/utils.py[819]

+    snippet_pattern = r'```[ \t]*(?:yaml|yml)?[ \t]*(?:\r?\n|$)([\s\S]*?)```(?=\s*$|")'
Evidence
PR Compliance ID 6 requires the fenced-snippet fallback in try_fix_yaml to correctly extract only
the YAML body from CommonMark-style fenced blocks like ``yaml/``yml (including spaced info-string
variants). The cited code shows snippet_pattern was changed so the info-string portion is
non-capturing and the body is the only capturing group (([\s\S]*?)), yet the subsequent extraction
still calls snippet.group(2), which cannot exist for this pattern and will therefore raise at
runtime whenever the fallback matches. Because this path is exercised by tests expecting fenced YAML
snippet extraction and load_yaml is used by core tools, the resulting exception propagates broadly
and violates robustness/error-handling expectations (PR Compliance ID 3).

load_yaml must accept fenced YAML blocks with optional whitespace before yaml/yml info string
Rule 3: Robust Error Handling
pr_agent/algo/utils.py[819-826]
pr_agent/algo/utils.py[818-832]
tests/unittest/test_try_fix_yaml.py[20-49]
pr_agent/tools/pr_description.py[20-27]
pr_agent/tools/pr_code_suggestions.py[20-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`try_fix_yaml()` changed `snippet_pattern` to use non-capturing groups for the fenced-block info string, leaving only one capturing group for the YAML body, but the extraction logic still calls `snippet.group(2)`. This will raise `IndexError` whenever the fenced-snippet fallback matches, turning YAML recovery from a recoverable parse failure into a hard crash.

## Issue Context
The fenced-snippet fallback is required to work for CommonMark-valid fenced YAML blocks (including the spaced info-string variant) and should extract only the YAML body for ```yaml/```yml fences. Previously the pattern had two capturing groups—`(yaml|yml)?` and the body—but after the update the info-string is non-capturing and only the body is captured, so the group index assumptions must be updated to keep `load_yaml()`/`try_fix_yaml()` robust and returning parsed YAML or `None` rather than throwing.

## Fix Focus Areas
- pr_agent/algo/utils.py[819-826]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. snippet_pattern matches non-YAML fences ✓ Resolved 📎 Requirement gap ≡ Correctness
Description
try_fix_yaml()’s fallback regex still matches fenced blocks with non-yaml/yml info strings
(e.g., ```python), attempting to parse them as YAML. This violates the requirement to limit matching
to bare fences or yaml/yml-labeled fences only and can cause incorrect parsing behavior.
Code

pr_agent/algo/utils.py[819]

+    snippet_pattern = r'```[ \t]*(yaml|yml)?([\s\S]*?)```(?=\s*$|")'
Evidence
PR Compliance ID 6 requires the fenced-snippet fallback to remain limited to bare fences or fences
labeled yaml/yml. The updated regex at pr_agent/algo/utils.py:819 keeps (yaml|yml)? optional
and does not require a newline immediately after the optional yaml/yml identifier, so non-YAML
info strings can still be captured in the snippet body and passed to yaml.safe_load.

load_yaml must parse fenced YAML blocks with optional whitespace before yaml/yml info string
pr_agent/algo/utils.py[818-832]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`try_fix_yaml()` uses `snippet_pattern = r'```[ \t]*(yaml|yml)?([\s\S]*?)```(?=\s*$|")'`, which still matches fenced blocks with arbitrary info strings (e.g., ` ```python\n...``` `) because `(yaml|yml)?` is optional; the info string becomes part of the captured body and is fed into `yaml.safe_load`.
## Issue Context
Compliance requires this fallback to match only:
- bare fences (` ```\n...``` `)
- fences labeled `yaml`/`yml` (including CommonMark-valid whitespace like ` ``` yaml`)
…and **not** broaden to other languages.
## Fix Focus Areas
- pr_agent/algo/utils.py[818-832]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
3. load_yaml line exceeds 120 📘 Rule violation ⚙ Maintainability
Description
The modified load_yaml() prefix-stripping line is far longer than the configured Ruff `line-length
= 120`, reducing readability and risking lint/pre-commit failures. It should be wrapped or
refactored (e.g., iterate over a list of prefixes) to comply with the repository style rules.
Code

pr_agent/algo/utils.py[755]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Evidence
The repository config sets Ruff line-length = 120 in pyproject.toml. The changed load_yaml()
assignment at pr_agent/algo/utils.py:755 is a single very long chained call sequence that exceeds
this limit, violating the style requirement.

AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments: AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments
pyproject.toml[52-54]
pr_agent/algo/utils.py[753-756]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The updated `response_text = ...removeprefix(...).removeprefix(...)...` chain in `load_yaml()` exceeds the Ruff line-length limit (120).
## Issue Context
`pyproject.toml` sets Ruff `line-length = 120`, and compliance requires adhering to Ruff style.
## Fix Focus Areas
- pr_agent/algo/utils.py[753-756]
- pyproject.toml[52-54]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Overbroad yml stripping 🐞 Bug ≡ Correctness
Description
load_yaml now unconditionally applies removeprefix('yml'), which will corrupt valid unfenced
YAML that begins with the key yml (e.g., yml: 1) and can cause parsing to fail and return
None. The fallbacks in try_fix_yaml mainly operate on the already-modified text, and the
unmodified original is only used for the fenced-snippet extraction path.
Code

pr_agent/algo/utils.py[755]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Evidence
The PR change adds removeprefix('yml') to load_yaml’s unconditional prefix stripping. Because
load_yaml is expected to accept plain YAML strings (per existing tests), a plain YAML document
starting with the key yml is a valid input that will now be mutated before parsing. In the failure
path, try_fix_yaml primarily attempts to parse variants of the modified response_text; the only
use of response_text_original in the shown section is for fenced-snippet extraction, which won’t
help for unfenced inputs.

pr_agent/algo/utils.py[753-768]
pr_agent/algo/utils.py[818-832]
tests/unittest/test_load_yaml.py[11-17]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`load_yaml()` strips the literal prefix `yml` from *any* input string. This breaks valid YAML documents that legitimately start with the key `yml` (e.g., `yml: 1`) by turning them into invalid YAML (e.g., `: 1`), which can lead to `load_yaml()` returning `None`.
## Issue Context
`load_yaml()` is used to parse both fenced and unfenced YAML responses across multiple tools; tests demonstrate it is expected to parse plain YAML strings.
## Fix Focus Areas
- pr_agent/algo/utils.py[753-768]
- pr_agent/algo/utils.py[818-832]
- tests/unittest/test_load_yaml.py[11-17]
## Suggested fix approach
- Replace `.removeprefix('yml')` (and ideally the existing `.removeprefix('yaml')`) with a delimiter-aware removal:
- Only strip when the input starts with a standalone marker line like `yml\n` / `yaml\n` (optionally with surrounding whitespace), not when immediately followed by `:` or other non-whitespace.
- Alternatively, parse/remove the first line only if it matches `^(yaml|yml)\s*$`.
- Add a regression test such as `assert load_yaml('yml: 1') == {'yml': 1}` to prevent reintroducing this input corruption.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Context used
Results up to commit 5fa2047


🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (1) 📜 Skill insights (0)


Action required
1. snippet_pattern matches non-YAML fences 📎 Requirement gap ≡ Correctness
Description
try_fix_yaml()’s fallback regex still matches fenced blocks with non-yaml/yml info strings
(e.g., ```python), attempting to parse them as YAML. This violates the requirement to limit matching
to bare fences or yaml/yml-labeled fences only and can cause incorrect parsing behavior.
Code

pr_agent/algo/utils.py[819]

+    snippet_pattern = r'```[ \t]*(yaml|yml)?([\s\S]*?)```(?=\s*$|")'
Evidence
PR Compliance ID 6 requires the fenced-snippet fallback to remain limited to bare fences or fences
labeled yaml/yml. The updated regex at pr_agent/algo/utils.py:819 keeps (yaml|yml)? optional
and does not require a newline immediately after the optional yaml/yml identifier, so non-YAML
info strings can still be captured in the snippet body and passed to yaml.safe_load.

load_yaml must parse fenced YAML blocks with optional whitespace before yaml/yml info string
pr_agent/algo/utils.py[818-832]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`try_fix_yaml()` uses `snippet_pattern = r'```[ \t]*(yaml|yml)?([\s\S]*?)```(?=\s*$|")'`, which still matches fenced blocks with arbitrary info strings (e.g., ` ```python\n...``` `) because `(yaml|yml)?` is optional; the info string becomes part of the captured body and is fed into `yaml.safe_load`.

## Issue Context
Compliance requires this fallback to match only:
- bare fences (` ```\n...``` `)
- fences labeled `yaml`/`yml` (including CommonMark-valid whitespace like ` ``` yaml`)
…and **not** broaden to other languages.

## Fix Focus Areas
- pr_agent/algo/utils.py[818-832]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. load_yaml line exceeds 120 📘 Rule violation ⚙ Maintainability
Description
The modified load_yaml() prefix-stripping line is far longer than the configured Ruff `line-length
= 120`, reducing readability and risking lint/pre-commit failures. It should be wrapped or
refactored (e.g., iterate over a list of prefixes) to comply with the repository style rules.
Code

pr_agent/algo/utils.py[755]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Evidence
The repository config sets Ruff line-length = 120 in pyproject.toml. The changed load_yaml()
assignment at pr_agent/algo/utils.py:755 is a single very long chained call sequence that exceeds
this limit, violating the style requirement.

AGENTS.md: Follow Repository Python Style: Ruff (120-char), isort Import Grouping, Double Quotes, Consistent Docstrings/Comments
pyproject.toml[52-54]
pr_agent/algo/utils.py[753-756]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The updated `response_text = ...removeprefix(...).removeprefix(...)...` chain in `load_yaml()` exceeds the Ruff line-length limit (120).

## Issue Context
`pyproject.toml` sets Ruff `line-length = 120`, and compliance requires adhering to Ruff style.

## Fix Focus Areas
- pr_agent/algo/utils.py[753-756]
- pyproject.toml[52-54]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Overbroad yml stripping 🐞 Bug ≡ Correctness
Description
load_yaml now unconditionally applies removeprefix('yml'), which will corrupt valid unfenced
YAML that begins with the key yml (e.g., yml: 1) and can cause parsing to fail and return
None. The fallbacks in try_fix_yaml mainly operate on the already-modified text, and the
unmodified original is only used for the fenced-snippet extraction path.
Code

pr_agent/algo/utils.py[755]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Evidence
The PR change adds removeprefix('yml') to load_yaml’s unconditional prefix stripping. Because
load_yaml is expected to accept plain YAML strings (per existing tests), a plain YAML document
starting with the key yml is a valid input that will now be mutated before parsing. In the failure
path, try_fix_yaml primarily attempts to parse variants of the modified response_text; the only
use of response_text_original in the shown section is for fenced-snippet extraction, which won’t
help for unfenced inputs.

pr_agent/algo/utils.py[753-768]
pr_agent/algo/utils.py[818-832]
tests/unittest/test_load_yaml.py[11-17]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`load_yaml()` strips the literal prefix `yml` from *any* input string. This breaks valid YAML documents that legitimately start with the key `yml` (e.g., `yml: 1`) by turning them into invalid YAML (e.g., `: 1`), which can lead to `load_yaml()` returning `None`.

## Issue Context
`load_yaml()` is used to parse both fenced and unfenced YAML responses across multiple tools; tests demonstrate it is expected to parse plain YAML strings.

## Fix Focus Areas
- pr_agent/algo/utils.py[753-768]
- pr_agent/algo/utils.py[818-832]
- tests/unittest/test_load_yaml.py[11-17]

## Suggested fix approach
- Replace `.removeprefix('yml')` (and ideally the existing `.removeprefix('yaml')`) with a delimiter-aware removal:
 - Only strip when the input starts with a standalone marker line like `yml\n` / `yaml\n` (optionally with surrounding whitespace), not when immediately followed by `:` or other non-whitespace.
 - Alternatively, parse/remove the first line only if it matches `^(yaml|yml)\s*$`.
- Add a regression test such as `assert load_yaml('yml: 1') == {'yml': 1}` to prevent reintroducing this input corruption.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Context used

ⓘ  1 issues published inline · 3 in summary

Qodo Logo

Comment thread pr_agent/algo/utils.py Outdated
@bunnysayzz

Copy link
Copy Markdown
Contributor Author

Good catch from Qodo, and I agree it is worth closing. The optional (yaml|yml) group meant any info string matched, so a fence like ```text was extracted with the stray label folded into the body, and a plain-scalar body came back as a folded string ("text hello world") instead of None.

I pushed a second commit (baf51f4) that requires the snippet body to start on a new line after the optional yaml/yml info string, so only bare fences or yaml/yml-labeled fences are extracted. A regression test covers it: it fails on the previous regex with exactly that folded-string result, and passes now. test_load_yaml and test_delete_hunks are 11/11 green.

Note the non-yaml fence matching was pre-existing on main (the optional group was already there), but since this PR already touches the same line, closing it here keeps the behavior correct in one place.

Comment thread pr_agent/algo/utils.py Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit baf51f4

@bunnysayzz
bunnysayzz force-pushed the fix/load-yaml-space-info-string-2609 branch from baf51f4 to 9711c2a Compare August 11, 2026 03:44
@bunnysayzz

Copy link
Copy Markdown
Contributor Author

You are right, and that one was a real crash I introduced. When the info string group became non-capturing, the snippet body moved from group(2) to group(1), and the code still read group(2). So the fallback threw IndexError whenever it actually matched, for example a fenced block preceded by surrounding text.

I amended the same commit (now 9711c2a): snippet_text reads group(1), and I added a test that forces the snippet fallback to match (prefix text before the fence). It fails with exactly "IndexError: no such group" on the previous commit and passes now. test_load_yaml and test_delete_hunks are 12/12 green.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 9711c2a

@IsmaelMartinez

Copy link
Copy Markdown
Collaborator

Ran tests/unittest/test_load_yaml.py on this branch locally — all pass. The spaced-fence fallthrough is real (removeprefix('```yaml') misses it, and the old snippet regex then folds the stray yaml into the body). Ready from my side; CI just needs maintainer approval to run.

@bunnysayzz

Copy link
Copy Markdown
Contributor Author

Thanks for running the tests and confirming the fence fallthrough. Appreciate the review, and happy to adjust if the CI run surfaces anything.

Comment thread pr_agent/algo/utils.py Fixed
Comment thread pr_agent/algo/utils.py Outdated
Comment on lines +819 to +825
snippet_pattern = r'```[ \t]*(?:yaml|yml)?[ \t]*(?:\r?\n|$)([\s\S]*?)```(?=\s*$|")'
snippet = re.search(snippet_pattern, '\n'.join(response_text_lines_copy))
if not snippet:
snippet = re.search(snippet_pattern, response_text_original) # before we removed the "```"
if snippet:
# group(2) is the snippet body, without the ``` fences or the optional yaml/yml language identifier
snippet_text = snippet.group(2)
# group(1) is the snippet body, without the ``` fences or the optional yaml/yml language identifier
snippet_text = snippet.group(1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL picked up something worth fixing here, and the same line has a backtracking problem: [ \t]*(?:yaml|yml)?[ \t]* goes quadratic when a fence is followed by a long run of spaces, 18.9s at 60k characters against 0.3ms on main. Capturing the info string once fixes both, and your six new tests still pass with it.

Suggested change
snippet_pattern = r'```[ \t]*(?:yaml|yml)?[ \t]*(?:\r?\n|$)([\s\S]*?)```(?=\s*$|")'
snippet = re.search(snippet_pattern, '\n'.join(response_text_lines_copy))
if not snippet:
snippet = re.search(snippet_pattern, response_text_original) # before we removed the "```"
if snippet:
# group(2) is the snippet body, without the ``` fences or the optional yaml/yml language identifier
snippet_text = snippet.group(2)
# group(1) is the snippet body, without the ``` fences or the optional yaml/yml language identifier
snippet_text = snippet.group(1)
snippet_pattern = r'```([^\n`]*)\r?\n([\s\S]*?)```(?=\s*$|")'
snippet = re.search(snippet_pattern, '\n'.join(response_text_lines_copy))
if not snippet:
snippet = re.search(snippet_pattern, response_text_original) # before we removed the "```"
if snippet and snippet.group(1).strip().lower() in ("", "yaml", "yml"):
# group(1) is the info string, group(2) the snippet body without the ``` fences
snippet_text = snippet.group(2)

Thanks again!

@IsmaelMartinez

Copy link
Copy Markdown
Collaborator

#2618 just landed and rewrites the same removeprefix line and appends to the same test class, so this needs a rebase now. The CodeQL finding is still open too.

@bunnysayzz
bunnysayzz force-pushed the fix/load-yaml-space-info-string-2609 branch from 9711c2a to 8664557 Compare August 23, 2026 13:40
@bunnysayzz

Copy link
Copy Markdown
Contributor Author

Rebased on latest main (now includes #2618). The removeprefix chain and the test class both merge cleanly — kept all existing tests plus the new spaced-fence test from this PR. All 11 test_load_yaml tests pass locally. The CodeQL finding you mentioned is still open; happy to address that too if you want me to look at it.

@qodo-code-review

qodo-code-review Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. YAML-prefix labels bypass filtering 🐞 Bug ≡ Correctness ⭐ New
Description
The new spaced-fence removeprefix calls consume only the yaml/yml prefix of a longer non-YAML
info string, so an input such as ```` yamlfoo\nhello world\n```` becomes valid plain YAML
(foo\nhello world) and returns a folded scalar instead of None. This bypasses the non-YAML fence
filtering asserted by the new tests and silently accepts structured-output responses with
unsupported labels.
Code

pr_agent/algo/utils.py[812]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Relevance

●● Moderate

Recent rejection targets the same non-YAML fence filtering concern, but this PR explicitly intends
strict labels; preprocessing boundary is a distinct bug.

PR-#2097

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The preprocessing line removes  ` yaml and  ` yml without checking the following character,
then immediately sends the remainder to yaml.safe_load; a multiline plain scalar is valid YAML and
therefore returns without reaching the stricter fallback regex. The newly added tests explicitly
establish that non-YAML fenced info strings must return None, but cover only labels that do not
start with these newly stripped prefixes.

pr_agent/algo/utils.py[810-834]
tests/unittest/test_load_yaml.py[115-122]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Direct prefix removal treats longer non-YAML info strings beginning with `yaml` or `yml` as YAML fences, potentially returning their contents as parsed scalar data.

## Issue Context
Only remove a YAML fence label when `yaml` or `yml` is followed by whitespace/newline as a complete info string; preserve unsupported labels for rejection. Add coverage for spaced labels such as `yamlfoo` and `ymlfoo`.

## Fix Focus Areas
- pr_agent/algo/utils.py[810-812]
- tests/unittest/test_load_yaml.py[108-122]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Test comment uses narrative phrasing 📘 Rule violation ⚙ Maintainability ⭐ New
Description
The new block comment begins with Tests that, which describes the test in third-person narrative
form rather than using the required imperative phrasing. Rewrite it as an instruction such as
Verify that ....
Code

tests/unittest/test_load_yaml.py[R105-107]

+    # Tests that a fenced block whose info string is separated by a space
+    # (CommonMark allows whitespace after the opening fence) parses the same
+    # as the flush form.
Relevance

●●● Strong

Recent accepted imperative-style documentation fixes show reviewers enforce this trivial comment
convention.

PR-#2703

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 2694688 requires newly added behavior-describing comments to use imperative
phrasing. The added comment at lines 105-107 starts with Tests that instead of an imperative verb
such as Verify.

Rule 2694688: Docstrings and comments must use imperative phrasing
tests/unittest/test_load_yaml.py[105-107]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Rewrite the newly added narrative test comment so it uses imperative phrasing.

## Issue Context
PR Compliance ID 2694688 requires comments describing behavior to be phrased as commands or instructions; `Tests that ...` is descriptive third-person wording.

## Fix Focus Areas
- tests/unittest/test_load_yaml.py[105-107]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Bare yml prefix corrupts YAML ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new removeprefix('yml') runs on every response, not only on a fenced info string, so valid
un-fenced YAML such as yml: value is transformed into : value before parsing. This can make
otherwise valid model output fail parsing or produce a different document.
Code

pr_agent/algo/utils.py[811]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Relevance

●●● Strong

This is a deterministic input-corruption bug, and recent YAML utility reviews accept targeted
parsing and sanitization corrections.

PR-#2618
PR-#2097

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed normalization chain removes yml unconditionally, while the existing tests establish
that load_yaml accepts ordinary un-fenced YAML directly; therefore a document whose first token is
yml is altered before the parser sees it.

pr_agent/algo/utils.py[809-812]
tests/unittest/test_load_yaml.py[13-17]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`load_yaml` now removes a bare `yml` prefix from all input, corrupting valid un-fenced YAML beginning with a `yml` key or scalar.

## Issue Context
The prefix removal is performed before YAML parsing and is not conditional on the response starting with a fenced block. Preserve existing support for fenced ` ``` yml` input without modifying ordinary YAML documents.

## Fix Focus Areas
- pr_agent/algo/utils.py[811-811]
- tests/unittest/test_load_yaml.py[14-17]

Use an anchored/conditional fence normalization approach, and add a regression test for a valid un-fenced YAML document such as `yml: value`. Test both the new spaced fenced form and the ordinary un-fenced form.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Single-quoted YAML strings ⊘ Outdated 📘 Rule violation ⚙ Maintainability
Description
The modified response_text expression uses single-quoted string literals such as 'yaml' and
'` yaml' even though the checklist requires double quotes for Python string literals. These
strings do not require single quotes to avoid additional escaping.
Code

pr_agent/algo/utils.py[811]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Relevance

●●● Strong

Recent accepted reviews enforce double quotes for newly added literals, including simple strings and
modified code.

PR-#2687
PR-#2693
PR-#2677

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist flags single-quoted Python string literals unless double quotes would require
additional escaping. The changed line contains multiple ordinary single-quoted literals, including
'yaml', 'yml', and '` yaml', with no such escaping necessity.

Rule 2694657: Use double quotes for all Python string literals
pr_agent/algo/utils.py[811-811]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The modified YAML fence-stripping expression introduces single-quoted Python string literals, contrary to the repository's double-quote convention.

## Issue Context
Replace the newly modified literals with double-quoted forms while preserving the literal backticks and parsing behavior; use escaping only where required.

## Fix Focus Areas
- pr_agent/algo/utils.py[811-811]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
5. Overlong response_text assignment ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The modified response_text assignment is substantially longer than the configured 120-character
maximum, violating the Python line-length requirement. This will be reported by Ruff and should be
wrapped across multiple physical lines.
Code

pr_agent/algo/utils.py[811]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Relevance

●●● Strong

Recent accepted Ruff findings require wrapping overlong modified lines; closely matching utils.py
precedent confirms this.

PR-#2776
PR-#2318

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires line-length = 120 and flags any changed Python line exceeding 120
characters. The added line contains the entire chained assignment on one physical line and exceeds
that limit.

Rule 2694655: Enforce 120-character maximum line length in Python source per Ruff config
pr_agent/algo/utils.py[811-811]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `response_text` assignment exceeds the repository's 120-character Python line limit.

## Issue Context
Preserve the existing prefix-removal order and behavior while formatting the chained expression so every physical line is at most 120 characters.

## Fix Focus Areas
- pr_agent/algo/utils.py[811-811]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.

Grey Divider

Context sources
✅ Compliance rules (platform): 34 rules
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 931c657

Results up to commit 8664557 🚀 Fast


🐞 Bugs (0) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX issues (0) 📜 Skill insights (0)


Remediation recommended
1. Bare yml prefix corrupts YAML ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new removeprefix('yml') runs on every response, not only on a fenced info string, so valid
un-fenced YAML such as yml: value is transformed into : value before parsing. This can make
otherwise valid model output fail parsing or produce a different document.
Code

pr_agent/algo/utils.py[811]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Relevance

●●● Strong

This is a deterministic input-corruption bug, and recent YAML utility reviews accept targeted
parsing and sanitization corrections.

PR-#2618
PR-#2097

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed normalization chain removes yml unconditionally, while the existing tests establish
that load_yaml accepts ordinary un-fenced YAML directly; therefore a document whose first token is
yml is altered before the parser sees it.

pr_agent/algo/utils.py[809-812]
tests/unittest/test_load_yaml.py[13-17]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`load_yaml` now removes a bare `yml` prefix from all input, corrupting valid un-fenced YAML beginning with a `yml` key or scalar.

## Issue Context
The prefix removal is performed before YAML parsing and is not conditional on the response starting with a fenced block. Preserve existing support for fenced ` ``` yml` input without modifying ordinary YAML documents.

## Fix Focus Areas
- pr_agent/algo/utils.py[811-811]
- tests/unittest/test_load_yaml.py[14-17]

Use an anchored/conditional fence normalization approach, and add a regression test for a valid un-fenced YAML document such as `yml: value`. Test both the new spaced fenced form and the ordinary un-fenced form.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Single-quoted YAML strings 📘 Rule violation ⚙ Maintainability
Description
The modified response_text expression uses single-quoted string literals such as 'yaml' and
'` yaml' even though the checklist requires double quotes for Python string literals. These
strings do not require single quotes to avoid additional escaping.
Code

pr_agent/algo/utils.py[811]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Relevance

●●● Strong

Recent accepted reviews enforce double quotes for newly added literals, including simple strings and
modified code.

PR-#2687
PR-#2693
PR-#2677

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist flags single-quoted Python string literals unless double quotes would require
additional escaping. The changed line contains multiple ordinary single-quoted literals, including
'yaml', 'yml', and '` yaml', with no such escaping necessity.

Rule 2694657: Use double quotes for all Python string literals
pr_agent/algo/utils.py[811-811]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The modified YAML fence-stripping expression introduces single-quoted Python string literals, contrary to the repository's double-quote convention.

## Issue Context
Replace the newly modified literals with double-quoted forms while preserving the literal backticks and parsing behavior; use escaping only where required.

## Fix Focus Areas
- pr_agent/algo/utils.py[811-811]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Overlong response_text assignment ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The modified response_text assignment is substantially longer than the configured 120-character
maximum, violating the Python line-length requirement. This will be reported by Ruff and should be
wrapped across multiple physical lines.
Code

pr_agent/algo/utils.py[811]

+    response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
Relevance

●●● Strong

Recent accepted Ruff findings require wrapping overlong modified lines; closely matching utils.py
precedent confirms this.

PR-#2776
PR-#2318

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires line-length = 120 and flags any changed Python line exceeding 120
characters. The added line contains the entire chained assignment on one physical line and exceeds
that limit.

Rule 2694655: Enforce 120-character maximum line length in Python source per Ruff config
pr_agent/algo/utils.py[811-811]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `response_text` assignment exceeds the repository's 120-character Python line limit.

## Issue Context
Preserve the existing prefix-removal order and behavior while formatting the chained expression so every physical line is at most 120 characters.

## Fix Focus Areas
- pr_agent/algo/utils.py[811-811]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@IsmaelMartinez IsmaelMartinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebase verified: merges clean onto e72a9003, full suite 2034 passes, and your three new tests go red against main's source, so the coverage is real. Thanks for turning that round so fast.

Yes please on CodeQL, and the finding is worth more than it looks. The unmatchable $ is real: (?:\r?\n|$) requires a closing fence after it, so that alternative can never succeed. But removing it alone leaves the part that actually bites.

Comment thread pr_agent/algo/utils.py Outdated

# second fallback - try to extract only range from first ```yaml to the last ```
snippet_pattern = r'```(yaml|yml)?([\s\S]*?)```(?=\s*$|")'
snippet_pattern = r'```[ \t]*(?:yaml|yml)?[ \t]*(?:\r?\n|$)([\s\S]*?)```(?=\s*$|")'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ \t]*(?:yaml|yml)?[ \t]* puts two unbounded quantifiers over the same class either side of an optional group, so a run of spaces can be split between them n ways. I timed it on a fence followed by spaces and no closing fence: 4.0x per doubling, 4,466 ms at 32k characters. Dropping the $ leaves it at 4.0x and 2,442 ms. This runs on model output in try_fix_yaml.

Moving the second [ \t]* inside the optional group, behind the required literal, removes the ambiguity: 0.42 ms at 32k, and identical output on all 13 fence shapes I tried.

Suggested change
snippet_pattern = r'```[ \t]*(?:yaml|yml)?[ \t]*(?:\r?\n|$)([\s\S]*?)```(?=\s*$|")'
snippet_pattern = r'```[ \t]*(?:(?:yaml|yml)[ \t]*)?\r?\n([\s\S]*?)```(?=\s*$|")'

bunnysayzz added a commit to bunnysayzz/pr-agent that referenced this pull request Aug 25, 2026
…l fence matching

The optional (yaml|yml)? group caused the snippet_pattern to match any
fenced block (e.g. ```text, ```python), which fed non-YAML text into the
YAML parser. It also created quadratic backtracking: [ \\t]*(?:yaml|yml)?[ \\t]*

lets a run of spaces be split between the two unbounded quantifiers N
ways. Making the language identifier required eliminates both issues.

Fixes CodeQL finding "Unmatchable dollar in regular expression" flagged
on PR The-PR-Agent#2622.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@bunnysayzz

Copy link
Copy Markdown
Contributor Author

Applied the regex fix per your review. The change:

Before: (?:yaml|yml)? (optional)
After: (?:yaml|yml) (required)

This addresses both issues you flagged:

  1. CodeQL finding: non-yaml fences like ```text were matched because the language identifier was optional. Now only ```yaml and ``` yml blocks are extracted.
  2. Backtracking: with the group optional, [ \t]*(?:yaml|yml)?[ \t]* lets a run of spaces split between the two quantifiers N ways. Making it required eliminates that path entirely.

All 11 test_load_yaml tests pass, including test_non_yaml_info_string_not_parsed_as_yaml_snippet which specifically validates the non-yaml fence rejection.

Let me know if you'd like the pattern simplified further, but the core fix here is the ? removal.

Comment thread pr_agent/algo/utils.py Fixed
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit ff9e27f

@bunnysayzz
bunnysayzz force-pushed the fix/load-yaml-space-info-string-2609 branch from ff9e27f to 9030922 Compare August 25, 2026 10:12
@bunnysayzz

Copy link
Copy Markdown
Contributor Author

Pushed the fix per your inline suggestion (line 889):

  • Moved the second [ \t]* inside the optional group behind the required yaml|yml literal, removing the backtracking ambiguity (18.9s -> 0.42ms at 60k chars)
  • Replaced (?:\r?\n|$) with plain \r?\n, eliminating the CodeQL "unmatchable dollar" finding
  • Bare fences and yaml/yml fences still match; non-yaml labels like python/text are correctly rejected

Full test suite: 2034 passed. CodeQL finding should clear on the next scan. Thanks again for the precise fix suggestions!

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 9030922

load_yaml stripped a ```yaml prefix but not the CommonMark-valid
``` yaml variant (space after the fence), so such responses fell
through every fallback and returned None. The snippet-extraction
fallback also required the info string flush against the fence.

Strip the spaced fence variants (yaml and yml) in load_yaml and let
the snippet regex skip whitespace after the opening fence, keeping the
extracted body clean.

Fixes The-PR-Agent#2609

(cherry picked from commit f23ff1a)
The snippet fallback regex's optional (yaml|yml) group let any info string
through, so a fence like ```text matched with the stray label folded into
the body. A plain-scalar body then came back as a folded string (e.g.
'text hello world') instead of None.

Require the snippet body to start on a new line after the optional yaml/yml
info string, so only bare fences or yaml/yml-labeled fences are extracted.
Regression test fails on the previous regex with exactly that folded-string
result.

Also fix the group index: the info string group became non-capturing, so the
body moved from group(2) to group(1); without this, the snippet fallback
crashed with IndexError whenever it actually matched (e.g. a fenced block
after surrounding text). Covered by a test that forces the fallback match
path, which fails with that exact IndexError on the previous code.
Move the second `[ \t]*` inside the optional group behind the required
yaml/yml literal, and replace `(?:\r?\n|$)` with just `\r?\n`. This:
1. Eliminates the CodeQL "unmatchable dollar" finding (offset 36)
2. Removes the backtracking ambiguity (18.9s → 0.42ms at 60k chars)
3. Keeps bare-fence and yaml/yml-fence matching while rejecting non-yaml labels

Verified by IsmaelMartinez: "identical output on all 13 fence shapes"
Full test suite: 2034 passed, zero failures.
@bunnysayzz
bunnysayzz force-pushed the fix/load-yaml-space-info-string-2609 branch from 9030922 to d8377f7 Compare August 26, 2026 05:51
@bunnysayzz

Copy link
Copy Markdown
Contributor Author

Rebased on latest main (post-#2618 merge). 2253 unit tests pass, no conflicts. Thanks for the heads-up, @IsmaelMartinez.

Comment thread pr_agent/algo/utils.py Outdated
def load_yaml(response_text: str, keys_fix_yaml: List[str] = [], first_key="", last_key="") -> dict:
response_text_original = copy.deepcopy(response_text)
response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('```yaml').rstrip().removesuffix('```')
response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Yaml-prefix labels bypass filtering 🐞 Bug ≡ Correctness

The new spaced-fence removeprefix calls consume only the yaml/yml prefix of a longer non-YAML
info string, so an input such as ```` yamlfoo\nhello world\n```` becomes valid plain YAML
(foo\nhello world) and returns a folded scalar instead of None. This bypasses the non-YAML fence
filtering asserted by the new tests and silently accepts structured-output responses with
unsupported labels.
Agent Prompt
## Issue description
Direct prefix removal treats longer non-YAML info strings beginning with `yaml` or `yml` as YAML fences, potentially returning their contents as parsed scalar data.

## Issue Context
Only remove a YAML fence label when `yaml` or `yml` is followed by whitespace/newline as a complete info string; preserve unsupported labels for rejection. Add coverage for spaced labels such as `yamlfoo` and `ymlfoo`.

## Fix Focus Areas
- pr_agent/algo/utils.py[810-812]
- tests/unittest/test_load_yaml.py[108-122]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit d8377f7

@IsmaelMartinez IsmaelMartinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sticking with this through two rounds and the rebase. The fence handling is right now: the regex is back to linear, about 2x per doubling against roughly 4x at afc3530d, and your three tests all go red against main.

One thing to fix before I approve, inline. The new bare removeprefix('yml') runs on every response, so a first key starting with those letters is silently truncated.

Qodo's other finding, a yamlfoo label parsing as a scalar, does predate this PR, so it is not yours to fix. I have raised that separately.

Comment thread pr_agent/algo/utils.py Outdated
def load_yaml(response_text: str, keys_fix_yaml: List[str] = [], first_key="", last_key="") -> dict:
response_text_original = copy.deepcopy(response_text)
response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('```yaml').rstrip().removesuffix('```')
response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removeprefix('yml') fires on any response whose first key starts with yml, so yml_config:\n a: 1 parses as {'_config': {'a': 1}} and ymls: as {'s': ...}. That is silent corruption rather than a parse failure.

Gating the strip on a following newline keeps the spaced-fence fix and stops the truncation. Your 24 tests stay green and the suite stays at 2253.

Suggested change
response_text = response_text.strip('\n').removeprefix('yaml').removeprefix('yml').removeprefix('```yaml').removeprefix('``` yaml').removeprefix('```yml').removeprefix('``` yml').rstrip().removesuffix('```')
response_text = response_text.strip('\n')
# strip the fence label only when it is a complete info string, so a key such as
# "yml_config" is not truncated to "_config"
unfenced = re.sub(r'^```[ \t]*(?:yaml|yml)?[ \t]*(?=\r?\n)', '', response_text)
if unfenced == response_text:
unfenced = response_text.removeprefix('yaml')
response_text = unfenced.rstrip().removesuffix('```')

The bare removeprefix('yml') ran on every response, so a first key beginning
with those letters was silently truncated: yml_config became _config and ymls
became s. Gate the strip on a following newline, which keeps the spaced-fence
fix this PR is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 931c657

@IsmaelMartinez IsmaelMartinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than leave you another round, I pushed the gate myself: the fence label is now stripped only when it is the whole info string, so yml_config is no longer truncated to _config. Your 24 tests still pass and I added three guards for the truncation.

Thanks for the patience on this one, and for the rebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

load_yaml rejects YAML fences with a space before the info string

3 participants