| # Copyright 2024 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from __future__ import annotations |
| |
| import re |
| from typing import Optional |
| |
| from crossbench.flags.base import Flags |
| |
| |
| class JSFlags(Flags): |
| """Custom flags implementation for V8 flags (--js-flags in chrome) |
| |
| Additionally to the base Flag implementation it asserts that bool flags |
| with the --no-.../--no... prefix are not contradicting each other. |
| """ |
| _NO_PREFIX = "--no" |
| _NAME_RE = re.compile(r"--[a-zA-Z_][a-zA-Z0-9_-]+") |
| |
| # We allow two forms: |
| # - space separated: --foo="1" --bar --baz='2' --boo=3 |
| # - comma separated: --foo="1",--bar ,--baz='2', --boo=3 |
| _VALUE_PATTERN = (r"('(?P<value_single_quotes>[^',]+)')|" |
| r"(\"(?P<value_double_quotes>[^\",]+)\")|" |
| r"(?P<value_no_quotes>[^'\", =]+)") |
| _END_OR_SEPARATOR_PATTERN = r"(\s*[,\s]\s*|$)" |
| _PARSE_RE = re.compile(fr"(?P<name>{_NAME_RE.pattern})" |
| fr"((?P<equal>=)({_VALUE_PATTERN})?)?" |
| fr"{_END_OR_SEPARATOR_PATTERN}") |
| |
| @classmethod |
| def parse_str(cls, raw_flags: str) -> JSFlags: |
| return cls._parse_str(raw_flags, "--js-flags") |
| |
| def copy(self) -> JSFlags: |
| return self.__class__(self) |
| |
| def _set(self, |
| flag_name: str, |
| flag_value: Optional[str] = None, |
| override: bool = False) -> None: |
| self._validate_js_flag_name(flag_name) |
| if flag_value is not None: |
| self._validate_js_flag_value(flag_name, flag_value) |
| self._check_negated_flag(flag_name, override) |
| super()._set(flag_name, flag_value, override) |
| |
| def _validate_js_flag_value(self, flag_name: str, flag_value: str) -> None: |
| if not isinstance(flag_value, str): |
| raise TypeError("JSFlag value must be str, " |
| f"but got {type(flag_value)}: {repr(flag_value)}") |
| if "," in flag_value: |
| raise ValueError( |
| "--js-flags: Comma in V8 flag value, flag escaping for chrome's " |
| f"--js-flags might not work: {flag_name}={repr(flag_value)}") |
| if self._WHITE_SPACE_RE.search(flag_value): |
| raise ValueError("--js-flags: V8 flag-values cannot contain whitespaces:" |
| f"{flag_name}={repr(flag_value)}") |
| |
| def _validate_js_flag_name(self, flag_name: str) -> None: |
| if not flag_name.startswith("--"): |
| raise ValueError("--js-flags: Only long-form flag names allowed, " |
| f"but got {repr(flag_name)}") |
| if not self._NAME_RE.fullmatch(flag_name): |
| raise ValueError(f"--js-flags: Invalid flag name {repr(flag_name)}. \n" |
| "Check invalid characters in the V8 flag name?") |
| |
| def _check_negated_flag(self, flag_name: str, override: bool) -> None: |
| if flag_name.startswith(self._NO_PREFIX): |
| enabled = flag_name[len(self._NO_PREFIX):] |
| # Check for --no-foo form |
| if enabled.startswith("-"): |
| enabled = enabled[1:] |
| enabled = "--" + enabled |
| if override: |
| del self[enabled] |
| elif enabled in self: |
| raise ValueError( |
| f"Conflicting flag {flag_name}, " |
| f"it has already been enabled by {repr(self._describe(enabled))}") |
| else: |
| # --foo => --no-foo |
| disabled = f"--no-{flag_name[2:]}" |
| if disabled not in self: |
| # Try compact version: --foo => --nofoo |
| disabled = f"--no{flag_name[2:]}" |
| if disabled not in self: |
| return |
| if override: |
| del self[disabled] |
| else: |
| raise ValueError(f"Conflicting flag {flag_name}, " |
| "it has previously been disabled by " |
| f"{repr(self._describe(flag_name))}") |
| |
| def __str__(self) -> str: |
| return ",".join(self) |