Skip to content

Commit cafb7a9

Browse files
authored
fix: loosen parsing logic for options in code examples and warn on fix (#345)
1 parent e98cf3e commit cafb7a9

File tree

4 files changed

+113
-8
lines changed

4 files changed

+113
-8
lines changed

exts/coding_guidelines/common.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import logging
2+
import re
3+
from typing import Dict, List, Tuple
24

35
from tqdm import tqdm
46

@@ -11,6 +13,64 @@ def get_tqdm(**kwargs):
1113
return tqdm(**kwargs)
1214

1315

16+
def sanitize_directive_content(content: str) -> Tuple[str, Dict[str, str], List[str]]:
17+
"""
18+
Detect and extract RST directive options incorrectly included in code content.
19+
20+
This handles cases where indentation issues in RST source files cause Sphinx's
21+
directive parser to not recognize directive options, resulting in them appearing
22+
in self.content instead of self.options.
23+
24+
For example, if the RST has:
25+
.. rust-example::
26+
:version: 1.79
27+
28+
use std::num::NonZero; # <- Less indented than option!
29+
30+
Sphinx will put ":version: 1.79" in content instead of options.
31+
32+
Args:
33+
content: The raw code content from a directive (joined self.content)
34+
35+
Returns:
36+
Tuple of (sanitized_code, extracted_options, raw_option_lines)
37+
- sanitized_code: Code with option-like lines removed from the start
38+
- extracted_options: Dict mapping option names to values
39+
- raw_option_lines: The original lines that were extracted (for error messages)
40+
"""
41+
lines = content.split('\n')
42+
extracted_options: Dict[str, str] = {}
43+
raw_option_lines: List[str] = []
44+
code_start_idx = 0
45+
46+
# Pattern to match directive options: :option_name: optional_value
47+
# This matches the same format Sphinx expects for directive options
48+
option_pattern = re.compile(r'^:(\w+):\s*(.*)$')
49+
50+
for i, line in enumerate(lines):
51+
stripped = line.strip()
52+
53+
# Skip blank lines at the start (between options and code)
54+
if not stripped:
55+
code_start_idx = i + 1
56+
continue
57+
58+
# Check if this looks like a directive option
59+
match = option_pattern.match(stripped)
60+
if match:
61+
opt_name = match.group(1)
62+
opt_value = match.group(2).strip()
63+
extracted_options[opt_name] = opt_value
64+
raw_option_lines.append(stripped)
65+
code_start_idx = i + 1
66+
else:
67+
# First non-option, non-blank line - actual code starts here
68+
break
69+
70+
sanitized_code = '\n'.join(lines[code_start_idx:])
71+
return sanitized_code, extracted_options, raw_option_lines
72+
73+
1474
# Get the Sphinx logger
1575
logger = logging.getLogger("sphinx")
1676
logger.setLevel(logging.WARNING)

exts/coding_guidelines/rust_examples.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from sphinx.errors import SphinxError
4242
from sphinx.util import logging
4343

44-
from .common import bar_format, get_tqdm
44+
from .common import bar_format, get_tqdm, sanitize_directive_content
4545

4646
logger = logging.getLogger(__name__)
4747

@@ -68,6 +68,13 @@ class MiriValidationError(SphinxError):
6868
# Warn mode values
6969
WARN_MODES = {"error", "allow"} # error = fail on warnings (default), allow = permit warnings
7070

71+
# Known directive options for rust-example (used for validation in sanitization)
72+
KNOWN_DIRECTIVE_OPTIONS = {
73+
"ignore", "compile_fail", "should_panic", "no_run",
74+
"miri", "warn", "edition", "channel", "version",
75+
"show_hidden", "name"
76+
}
77+
7178

7279
class RustExamplesConfig:
7380
"""Configuration loaded from rust_examples_config.toml"""
@@ -479,12 +486,53 @@ def run(self) -> List[nodes.Node]:
479486
config = RustExamplesConfig()
480487
env.rust_examples_config = config
481488

489+
# Get source location early (needed for error messages)
490+
source, line = self.state_machine.get_source_and_line(self.lineno)
491+
492+
# Parse the code content
493+
raw_code = '\n'.join(self.content)
494+
495+
# Sanitize content - detect and extract misplaced directive options
496+
# This handles RST indentation issues where Sphinx puts options into content
497+
raw_code, extracted_options, raw_option_lines = sanitize_directive_content(raw_code)
498+
499+
if extracted_options:
500+
# Log a detailed warning about the indentation issue
501+
opt_names = list(extracted_options.keys())
502+
503+
# Build informative message with context
504+
warning_parts = [
505+
f"{source}:{line}: Found directive options in code content "
506+
f"(RST indentation issue): {opt_names}.",
507+
"These options were extracted and will be applied, but please fix the source file.",
508+
"The code content should be indented at least as much as the options."
509+
]
510+
511+
# Add specific context for version-related options
512+
if 'version' in extracted_options:
513+
extracted_version = extracted_options['version']
514+
warning_parts.append(
515+
f"Note: Extracted :version: {extracted_version} "
516+
f"(config default: {config.version}, "
517+
f"mismatch threshold: {config.version_mismatch_threshold} minor versions)"
518+
)
519+
520+
logger.warning(" ".join(warning_parts))
521+
522+
# Merge extracted options into self.options
523+
# Sphinx-parsed options take precedence (they were properly formatted)
524+
for opt_name, opt_value in extracted_options.items():
525+
if opt_name not in self.options:
526+
# Handle flag-style options (empty value means flag is set)
527+
if opt_name in ('ignore', 'no_run', 'show_hidden') and opt_value == '':
528+
self.options[opt_name] = None # Flag style
529+
else:
530+
self.options[opt_name] = opt_value
531+
482532
# Get configuration for showing hidden lines
483533
show_hidden_global = getattr(env.config, 'rust_examples_show_hidden', False)
484534
show_hidden = 'show_hidden' in self.options or show_hidden_global
485535

486-
# Parse the code content
487-
raw_code = '\n'.join(self.content)
488536
display_code, full_code, hidden_line_numbers = process_hidden_lines(raw_code, show_hidden)
489537

490538
# Determine rustdoc attribute
@@ -509,9 +557,6 @@ def run(self) -> List[nodes.Node]:
509557
miri_pattern = None
510558
has_miri_option = 'miri' in self.options
511559

512-
# Store source location (needed for error messages)
513-
source, line = self.state_machine.get_source_and_line(self.lineno)
514-
515560
if has_miri_option:
516561
miri_mode, miri_pattern = parse_miri_option(self.options.get('miri', ''))
517562

src/coding-guidelines/expressions/gui_7y0GAMmtMhch.rst.inc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
in this compliant example because the result of the division expression is an unsigned integer type.
9595

9696
.. rust-example::
97-
:version: 1.79
97+
:version: 1.79
9898

9999
use std::num::NonZero;
100100

src/coding-guidelines/expressions/gui_RHvQj8BHlz9b.rst.inc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
The use of this function is noncompliant because it does not detect out-of-range shifts.
152152

153153
.. rust-example::
154-
:version: 1.87
154+
:version: 1.87
155155

156156
fn main() {
157157
let bits : u32 = 61;

0 commit comments

Comments
 (0)