Option C Tools
Loading...
Searching...
No Matches
parsing.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oct/core/parsing.py
4
5"""
6Purpose
7-------
8Provide shared Python-source parsing helpers:
9
10- :func:`safe_parse` parses source text into an AST while capturing any
11 ``SyntaxWarning`` diagnostics (so callers can surface them without
12 letting them leak to stderr).
13- :func:`header_boundary` (OI-509 / FS-505) returns the line index where
14 file content begins, scanning at most the first 5 lines in the strict
15 order shebang → encoding → file-identity comment → blank line. This
16 replaces two drift-prone inline copies that previously existed in the
17 linter and the formatter.
18
19Responsibilities
20----------------
21- Parse Python source text into an AST tree.
22- Capture ``SyntaxWarning`` (e.g. invalid escape sequences) as structured data.
23- Locate the header/content boundary of an Option-C-formatted Python file
24 using only the file's basename and (optionally) its expected identity
25 comment, so both the linter (with full ``LinterContext``) and the
26 formatter (with a lighter ``FormatterContext``) can share one code path.
27
28Diagnostics
29-----------
30Domain: OCT-CORE
31Levels:
32 L2 — lifecycle
33 L3 — semantic details
34 L4 — deep tracing
35
36Contracts
37---------
38- :func:`safe_parse` must not emit warnings to stderr; all warnings are
39 returned as strings.
40- :func:`safe_parse` must return ``None`` for the tree on ``SyntaxError``.
41- :func:`header_boundary` must never raise; it returns an integer in
42 ``[0, min(len(lines), 5)]``.
43- :func:`header_boundary` must consume the trailing blank only if at
44 least one header element was matched first.
45"""
46
47from __future__ import annotations
48
49import ast
50import re
51import warnings
52from pathlib import Path
53
54
55_ENCODING_RE = re.compile(r"#.*coding[:=]")
56
57
58def safe_parse(text: str) -> tuple[ast.Module | None, list[str]]:
59 """Parse Python source, capturing SyntaxWarnings instead of emitting to stderr.
60
61 Returns (tree, warnings_list). tree is None on SyntaxError.
62 """
63 with warnings.catch_warnings(record=True) as w:
64 warnings.simplefilter("always", SyntaxWarning)
65 try:
66 tree = ast.parse(text)
67 except SyntaxError:
68 return None, []
69
70 caught = [str(warning.message) for warning in w
71 if issubclass(warning.category, SyntaxWarning)]
72 return tree, caught
73
74
76 lines: list[str],
77 path: Path,
78 expected_identity: str | None = None,
79) -> int:
80 """Return the line index where file content begins.
81
82 Scans at most the first 5 lines of *lines*, in strict order:
83 shebang → encoding → file-identity comment → blank line.
84
85 Parameters
86 ----------
87 lines : list[str]
88 Source file split by newlines (as returned by ``str.splitlines()``).
89 path : Path
90 The file's path; its basename is used as the fallback identity
91 match when *expected_identity* is not provided.
92 expected_identity : str | None, optional
93 The exact expected file-identity comment (e.g.
94 ``"# oct/core/parsing.py"``). When supplied the match is strict
95 against this string; otherwise any ``"# ...<path.name>"`` line
96 qualifies.
97 """
98 limit = min(len(lines), 5)
99 i = 0
100
101 # 1. Shebang (any flavour: python, python3, etc.)
102 if i < limit and lines[i].strip().startswith("#!"):
103 i += 1
104
105 # 2. Encoding declaration (PEP 263)
106 if i < limit and _ENCODING_RE.match(lines[i].strip()):
107 i += 1
108
109 # 3. File-identity comment — only if it references THIS file's name
110 if i < limit:
111 stripped = lines[i].strip()
112 if expected_identity is not None:
113 is_identity = (
114 stripped == expected_identity
115 or (stripped.startswith("# ") and stripped.endswith(path.name))
116 )
117 else:
118 is_identity = (
119 stripped.startswith("# ") and stripped.endswith(path.name)
120 )
121 if is_identity:
122 i += 1
123
124 # 4. Trailing blank line (only if at least one header element was found)
125 if i > 0 and i < limit and lines[i].strip() == "":
126 i += 1
127
128 return i
int header_boundary(list[str] lines, Path path, str|None expected_identity=None)
Definition parsing.py:79
tuple[ast.Module|None, list[str]] safe_parse(str text)
Definition parsing.py:58