99
1010import re
1111from pathlib import Path
12- from typing import Set
12+ from typing import Optional , Set
1313
1414from codeanalyzer .entrypoints .rules import RuleSet
1515from codeanalyzer .schema .py_schema import PyApplication
1616
1717_REQ = re .compile (r"^\s*['\"]?([A-Za-z0-9_.\-]+)" )
18- _DEPS_ARRAY = re .compile (r"dependencies\s*=\s*\[(.*?)\]" , re . DOTALL )
18+ _DEPS_START = re .compile (r"dependencies\s*=\s*\[" )
1919_PKG = re .compile (r"['\"]([A-Za-z0-9][A-Za-z0-9_.\-]*)" )
2020
2121
@@ -45,10 +45,11 @@ def _manifest_packages(project_dir: Path) -> Set[str]:
4545 out : Set [str ] = set ()
4646 pyproject = project_dir / "pyproject.toml"
4747 if pyproject .exists ():
48- # PEP 621 `[project] dependencies = [...]` -- single- or multi-line.
49- m = _DEPS_ARRAY .search (pyproject .read_text ())
50- if m :
51- for pm in _PKG .finditer (m .group (1 )):
48+ # PEP 621 `[project] dependencies = [...]` -- single- or multi-line,
49+ # possibly containing nested `[...]` extras (`celery[redis]`).
50+ span = _deps_array_span (_strip_comments (pyproject .read_text ()))
51+ if span is not None :
52+ for pm in _PKG .finditer (span ):
5253 out .add (pm .group (1 ).split ("[" , 1 )[0 ].lower ())
5354 requirements = project_dir / "requirements.txt"
5455 if requirements .exists ():
@@ -57,3 +58,52 @@ def _manifest_packages(project_dir: Path) -> Set[str]:
5758 if m :
5859 out .add (m .group (1 ).split ("[" , 1 )[0 ].lower ())
5960 return out
61+
62+
63+ def _strip_comments (text : str ) -> str :
64+ """Drop everything from an unquoted ``#`` to end of line.
65+
66+ # ponytail: quote tracking resets each line, so a `#` inside a
67+ # triple-quoted string spanning lines could be mis-stripped. TOML
68+ # dependency arrays don't use those in practice; revisit if they do.
69+ """
70+ out_lines = []
71+ for line in text .splitlines ():
72+ in_str = None
73+ cut = len (line )
74+ for i , ch in enumerate (line ):
75+ if in_str :
76+ if ch == in_str :
77+ in_str = None
78+ elif ch in ("'" , '"' ):
79+ in_str = ch
80+ elif ch == "#" :
81+ cut = i
82+ break
83+ out_lines .append (line [:cut ])
84+ return "\n " .join (out_lines )
85+
86+
87+ def _deps_array_span (text : str ) -> Optional [str ]:
88+ """Return the contents between the `dependencies = [` and its matching
89+ `]`, counting bracket depth so a nested `[...]` (extras, e.g.
90+ `celery[redis]`) doesn't close the span early."""
91+ m = _DEPS_START .search (text )
92+ if not m :
93+ return None
94+ depth = 1
95+ in_str = None
96+ i = m .end ()
97+ while i < len (text ) and depth > 0 :
98+ ch = text [i ]
99+ if in_str :
100+ if ch == in_str :
101+ in_str = None
102+ elif ch in ("'" , '"' ):
103+ in_str = ch
104+ elif ch == "[" :
105+ depth += 1
106+ elif ch == "]" :
107+ depth -= 1
108+ i += 1
109+ return text [m .end () : i - 1 ]
0 commit comments