3"""Inventory active warning controls in shell, CMake, Make, and YAML syntax."""
5from __future__
import annotations
8from dataclasses
import dataclass
9from pathlib
import Path
11from suppression_catalog
import (
18from suppression_hash_lex
import hash_lines
19from suppression_model
import Suppression
21_COMPILER_NAME =
r"(?:[A-Za-z0-9_.+-]*-)?(?:cc|c\+\+|gcc|g\+\+|clang|clang\+\+)"
22_COMPILER_CONTEXT_RE = re.compile(
23 r"\b[A-Za-z0-9_]*(?:CFLAGS|CXXFLAGS|CPPFLAGS|COMPILE_OPTIONS)\b"
24 r"|--extra-arg(?:-before)?=|(?:^|[\s(])-(?:D|I|f|std=)",
27_COMPILER_OWNER_RE = re.compile(
28 r"\b[A-Za-z0-9_]*(?:CFLAGS|CXXFLAGS|CPPFLAGS|COMPILE_OPTIONS)\b"
29 r"|--extra-arg(?:-before)?=",
32_COMPILER_OPTION_RE = re.compile(
r"(?:^|[\s(])-(?:D|I|f|std=)")
33_CMAKE_VARIABLES = frozenset((
"$CMAKE",
"$" +
"{CMAKE}",
"$" +
"{CMAKE_COMMAND}"))
34_CMAKE_WARNING_SUPPRESSIONS = frozenset(
38 "-Wno-error=deprecated",
42_COMMAND_PREFIXES = frozenset(
43 {
"!",
"COMMAND",
"command",
"do",
"elif",
"env",
"exec",
"if",
"then"}
45_SHELL_OPERATORS = frozenset({
"&",
"&&",
"(",
")",
";",
";;",
"|",
"||"})
46_YAML_KEY_RE = re.compile(
r"^\s*(?:-\s*)?(?P<key>[A-Za-z0-9_.-]+)\s*:\s*(?P<value>.*)$")
47_YAML_BLOCK_RE = re.compile(
48 r"^(?P<indent>\s*)(?:-\s*)?(?P<key>[A-Za-z0-9_.-]+)\s*:\s*"
49 r"(?P<indicator>[>|])(?P<modifiers>(?:[+-][1-9]?|[1-9][+-]?))?"
52_CMAKE_COMPILER_RE = re.compile(
53 r"\b(?:add_compile_options|target_compile_options)\s*\("
54 r"|\b(?:CMAKE_[A-Za-z0-9_]*(?:C|CXX|ASM)[A-Za-z0-9_]*FLAGS|COMPILE_OPTIONS)\b"
55 r"|\b(?:set|list)\s*\([^\n)]*(?:WNO[A-Za-z0-9_]*|C_FLAGS|CXX_FLAGS|WARNING_FLAGS)\b",
58_CMAKE_NONCONFIGURE_RE = re.compile(
59 r"(?:^|\s)(?:--build|--install|--open|--find-package|--help(?:-[A-Za-z-]+)?|"
60 r"--version|--system-information|-E|-P)(?=$|\s)"
64@dataclass(frozen=True)
66 """One flag-bearing source line and the context that owns it."""
72 blanket_is_compiler: bool
77@dataclass(frozen=True)
79 """One source-mapped line from a YAML-owned shell command."""
88@dataclass(frozen=True)
90 """One quote-decoded shell word or command-separating operator."""
95 operator: bool =
False
98@dataclass(frozen=True)
100 """One compiler or CMake executable occupying a command-word position."""
107def _shell_tokens(source: str) -> list[ShellToken]:
108 """Tokenize shell command words with quote and backslash semantics."""
109 tokens: list[ShellToken] = []
111 while index < len(source):
112 if source[index].isspace():
115 pair = source[index : index + 2]
116 if pair
in _SHELL_OPERATORS:
117 tokens.append(ShellToken(pair, index, index + 2, operator=
True))
120 if source[index]
in _SHELL_OPERATORS:
121 tokens.append(ShellToken(source[index], index, index + 1, operator=
True))
125 value: list[str] = []
126 while index < len(source):
128 if char.isspace()
or char
in _SHELL_OPERATORS:
130 if char
in {
"'",
'"'}:
133 while index < len(source)
and source[index] != quote:
135 if char ==
"\\" and quote ==
'"' and index + 1 < len(source):
136 escaped = source[index + 1]
137 if escaped
in {
'"',
"$", chr(96),
"\\",
"\n"}:
138 value.append(escaped)
143 if index >= len(source):
147 if char ==
"\\" and index + 1 < len(source):
148 value.append(source[index + 1])
153 tokens.append(ShellToken(
"".join(value), start, index))
157def _tool_kind(value: str) -> str |
None:
158 """Classify one decoded command word by executable basename."""
159 if value
in _CMAKE_VARIABLES:
161 basename = re.split(
r"[/\\]", value)[-1]
162 if basename ==
"cmake":
164 if re.fullmatch(_COMPILER_NAME, basename, re.IGNORECASE)
is not None:
169def _is_assignment(value: str) -> bool:
170 """Return whether a shell word is an environment assignment."""
171 return re.fullmatch(
r"[A-Za-z_][A-Za-z0-9_]*=.*", value, re.DOTALL)
is not None
174def _tool_commands(source: str) -> list[ToolCommand]:
175 """Return tools in executable positions, excluding data arguments."""
176 commands: list[ToolCommand] = []
177 expect_command =
True
179 active_kind: str |
None =
None
181 for token
in _shell_tokens(source):
183 expect_command =
True
188 basename = re.split(
r"[/\\]", token.value)[-1]
190 if _is_assignment(token.value):
192 if basename
in _COMMAND_PREFIXES:
195 if wrapper
and token.value.startswith(
"-"):
197 kind = _tool_kind(token.value)
199 commands.append(ToolCommand(kind, token.start, token.end))
201 expect_command =
False
205 if active_kind ==
"cmake" and token.value ==
"-E":
207 elif active_kind ==
"cmake" and cmake_dash_e
and basename ==
"env":
208 expect_command =
True
215def _has_compiler_context(source: str) -> bool:
216 """Return whether source owns a compiler command or option context."""
217 return _COMPILER_CONTEXT_RE.search(source)
is not None or any(
218 command.kind ==
"compiler" for command
in _tool_commands(source)
222def _cmake_commands(source: str) -> list[ToolCommand]:
223 """Return source-mapped CMake executables in command positions."""
224 return [command
for command
in _tool_commands(source)
if command.kind ==
"cmake"]
227def _has_cmake_context(source: str) -> bool:
228 """Return whether source contains an active CMake command."""
229 return bool(_cmake_commands(source))
232def _is_cmake_configure_context(source: str) -> bool:
233 """Return whether source invokes CMake's configure mode."""
234 commands = _cmake_commands(source)
237 invocation = source[commands[-1].end :]
238 return _CMAKE_NONCONFIGURE_RE.search(invocation)
is None
241def _control_kind_at(source: str, flag_position: int) -> str |
None:
242 """Return the nearest active compiler/CMake command owning one flag."""
243 prefix = source[:flag_position]
244 compiler_positions = [match.start()
for match
in _COMPILER_OWNER_RE.finditer(prefix)]
245 compiler_positions.extend(
246 command.start
for command
in _tool_commands(prefix)
if command.kind ==
"compiler"
248 compiler_positions.extend(match.start()
for match
in _CMAKE_COMPILER_RE.finditer(prefix))
249 cmake_matches = _cmake_commands(prefix)
250 compiler_position = max(compiler_positions, default=-1)
251 cmake_match = cmake_matches[-1]
if cmake_matches
else None
252 if cmake_match
is not None and cmake_match.start > compiler_position:
253 invocation = prefix[cmake_match.end :]
254 if _CMAKE_NONCONFIGURE_RE.search(invocation)
is None:
257 if compiler_position >= 0:
259 if _COMPILER_OPTION_RE.search(prefix)
is not None:
264def _is_cmake_warning_control(source: str, flag_position: int, flag: str) -> bool:
265 """Return whether one exact CMake diagnostic option owns the match."""
266 if flag
not in _CMAKE_WARNING_SUPPRESSIONS:
269 not token.operator
and token.value == flag
and token.start <= flag_position < token.end
270 for token
in _shell_tokens(source)
274def _folded_yaml_commands(
275 block: list[tuple[int, str, int]], content_indent: int
276) -> list[YamlCommandLine]:
277 """Join folded YAML paragraphs while retaining source and rationale."""
278 commands: list[YamlCommandLine] = []
279 group: list[tuple[int, str, int]] = []
283 nonlocal pending_reason
286 pieces: list[str] = []
287 spans: list[tuple[int, int, int]] = []
289 for line_no, content, _indent
in group:
293 pieces.append(content)
294 cursor += len(content)
295 spans.append((line_no, start, cursor))
296 joined =
" ".join(pieces)
297 lexical_lines, _ = hash_lines(
"yaml-command.sh", joined +
"\n")
298 lexical = lexical_lines[0]
if lexical_lines
else None
299 active = lexical.code
if lexical
is not None else ""
300 comment = lexical.comment.strip()
if lexical
is not None else ""
301 if not active.strip():
303 _inline_reason(comment)
if comment.startswith(
"Suppression rationale:")
else ""
307 reason = _inline_reason(comment)
or pending_reason
308 for line_no, start, end
in spans:
309 active_end =
min(end, len(active))
310 code = active[start:active_end]
if start < active_end
else ""
324 _line_no, content, indent = source
325 if not content.strip()
or indent > content_indent:
336def _yaml_command_lines(text: str) -> list[YamlCommandLine]:
337 """Return de-indented shell owned only by active run/shell block keys."""
338 raw_lines = text.splitlines()
339 commands: list[YamlCommandLine] = []
341 while index < len(raw_lines):
342 header = _YAML_BLOCK_RE.match(raw_lines[index])
346 header_indent = len(header.group(
"indent"))
347 key = header.group(
"key").lower()
348 block: list[tuple[int, str, int]] = []
350 content_indent: int |
None =
None
351 while index < len(raw_lines):
352 raw = raw_lines[index]
353 indent = len(raw) - len(raw.lstrip(
" "))
355 if content_indent
is None:
356 if indent <= header_indent:
358 content_indent = indent
359 elif indent < content_indent:
361 if content_indent
is not None:
362 block.append((index + 1, raw[content_indent:], indent))
363 elif not raw.strip():
364 block.append((index + 1,
"", 0))
366 if key
not in {
"run",
"shell"}
or content_indent
is None:
368 if header.group(
"indicator") ==
">":
369 commands.extend(_folded_yaml_commands(block, content_indent))
373 shell_text =
"\n".join(line
for _, line, _
in block) +
"\n"
374 shell_lines, _ = hash_lines(
"yaml-command.sh", shell_text)
375 for line_index, (source, lexical)
in enumerate(zip(block, shell_lines, strict=
True)):
376 reason = _inline_reason(lexical.comment)
or _structured_reason_above(
377 shell_lines, line_index
390def _active_build_code(path: str, code: str, inherited_context: str =
"") -> tuple[str, bool, int]:
391 """Return flag-bearing code, blanket -w status, and its source offset."""
393 if item.name ==
"CMakeLists.txt" or item.suffix ==
".cmake":
394 source = f
"{inherited_context} {code}"
395 active = _CMAKE_COMPILER_RE.search(source)
is not None or _is_cmake_configure_context(
398 return (code,
True, 0)
if active
else (
"",
False, 0)
399 if item.suffix
in {
".yaml",
".yml"}:
400 match = _YAML_KEY_RE.match(code)
403 key = match.group(
"key").lower()
404 value = match.group(
"value")
405 flag_key = key.endswith((
"cflags",
"cxxflags",
"cppflags",
"compile_options"))
406 command_key = key
in {
"command",
"run",
"shell"}
407 command_context = _has_compiler_context(value)
or _has_cmake_context(value)
408 if flag_key
or (command_key
and command_context):
409 return value,
True, match.start(
"value")
411 active_source = f
"{inherited_context} {code}"
412 active = _has_compiler_context(active_source)
or _has_cmake_context(active_source)
413 return (code,
True, 0)
if active
else (
"",
False, 0)
416def _inline_reason(comment: str) -> str:
417 """Return an attached reason, normalizing the structured prefix."""
418 prefix =
"Suppression rationale:"
419 reason = comment.strip()
422 if reason.startswith(prefix):
423 return reason.removeprefix(prefix).strip()
427def _structured_reason_above(lines: list[object], index: int) -> str:
428 """Return one explicit rationale block immediately above a command."""
429 prefix =
"Suppression rationale:"
430 notes: list[str] = []
431 for candidate
in reversed(lines[:index]):
432 code = getattr(candidate,
"code",
"")
433 comment = getattr(candidate,
"comment",
"").strip()
434 if code.strip()
or not comment:
436 notes.append(comment)
438 for note_index, note
in enumerate(notes):
439 if not note.startswith(prefix):
441 first = note.removeprefix(prefix).strip()
444 return " ".join([first, *notes[note_index + 1 :]]).strip()
448def _reason_concerns(flag: str, reason: str) -> tuple[str, ...]:
449 """Report missing reasons and retain the blanket-warning concern."""
450 concerns = []
if reason
else [
"blank-reason"]
452 concerns.append(
"broad-rule")
453 return tuple(concerns)
457 records: list[Suppression],
458 source: ActiveBuildCode,
460 """Append warning-control records with truthful CMake/compiler ownership."""
461 matches = list(WARNING_FLAG_RE.finditer(source.code))
462 if source.blanket_is_compiler:
463 matches.extend(BLANKET_WARNING_RE.finditer(source.code))
464 reason = _inline_reason(source.reason)
465 for match
in sorted(matches, key=
lambda item: item.start()):
466 flag = match.group(
"flag")
467 code_position = source.context.rfind(source.code)
468 if code_position < 0:
470 control_kind = _control_kind_at(source.context, code_position + match.start())
471 if control_kind
is None:
473 flag_position = code_position + match.start()
474 cmake_control = control_kind ==
"cmake" and _is_cmake_warning_control(
475 source.context, flag_position, flag
477 family =
"cmake" if cmake_control
else "compiler"
482 source.source_offset + match.start() + 1,
487 "configure-command" if cmake_control
else "build-target",
490 ownership(source.path),
491 _reason_concerns(flag, reason),
496def _continued_build_context(
503 """Return the active command context carried to the next source line."""
504 code = getattr(line,
"code",
"")
505 active_source = f
"{current} {code}".strip()
506 if shell_control
and code.rstrip().endswith(
"\\"):
507 return f
"{current} {code.rstrip()[:-1]}".strip()
510 and (current
or code.count(
"(") > code.count(
")"))
511 and active_source.count(
"(") > active_source.count(
")")
517def _append_line_build_records(
518 records: list[Suppression], path: str, text: str, first_line: str
520 """Append controls found in ordinary build-control source lines."""
521 lines, _ = hash_lines(path, text)
522 shell_control = is_shell_control(path, first_line)
523 cmake_control = Path(path).name ==
"CMakeLists.txt" or Path(path).suffix ==
".cmake"
524 continued_context =
""
525 continued_reason =
""
526 for index, line
in enumerate(lines):
527 structured_reason = _structured_reason_above(lines, index)
528 line_reason = _inline_reason(line.comment)
or continued_reason
or structured_reason
529 code, blanket_is_compiler, source_offset = _active_build_code(
530 path, line.code, continued_context
532 active_source = f
"{continued_context} {line.code}"
539 source_offset=source_offset,
540 blanket_is_compiler=blanket_is_compiler,
541 context=active_source,
545 next_context = _continued_build_context(
548 shell_control=shell_control,
549 cmake_control=cmake_control,
551 if next_context
and not continued_context:
552 continued_reason = _inline_reason(line.comment)
or structured_reason
553 elif not next_context:
554 continued_reason =
""
555 continued_context = next_context
558def _append_yaml_build_records(records: list[Suppression], path: str, text: str) ->
None:
559 """Append controls from YAML-owned shell blocks with source-line identity."""
561 for command
in _yaml_command_lines(text):
562 active_source = command.context
or f
"{block_context} {command.code}"
563 active = _has_compiler_context(active_source)
or _is_cmake_configure_context(active_source)
571 source_offset=command.source_offset,
572 blanket_is_compiler=
True,
573 context=active_source,
574 reason=command.reason,
579 elif command.code.rstrip().endswith(
"\\"):
580 block_context = f
"{block_context} {command.code.rstrip()[:-1]}".strip()
585def compiler_records(path: str, text: str) -> list[Suppression]:
586 """Inventory active warning-disable flags in build-control syntax."""
587 first_line = text.partition(
"\n")[0]
588 if not is_build_control(path, first_line):
590 records: list[Suppression] = []
591 _append_line_build_records(records, path, text, first_line)
592 if Path(path).suffix
in {
".yaml",
".yml"}:
593 _append_yaml_build_records(records, path, text)
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.