1# Copyright 2024 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5from __future__ import annotations 6 7import re 8from typing import Optional 9 10from crossbench.flags.base import Flags 11 12 13class JSFlags(Flags): 14 """Custom flags implementation for V8 flags (--js-flags in chrome) 15 16 Additionally to the base Flag implementation it asserts that bool flags 17 with the --no-.../--no... prefix are not contradicting each other. 18 """ 19 _NO_PREFIX = "--no" 20 _NAME_RE = re.compile(r"--[a-zA-Z_][a-zA-Z0-9_-]+") 21 22 # We allow two forms: 23 # - space separated: --foo="1" --bar --baz='2' --boo=3 24 # - comma separated: --foo="1",--bar ,--baz='2', --boo=3 25 _VALUE_PATTERN = (r"('(?P<value_single_quotes>[^',]+)')|" 26 r"(\"(?P<value_double_quotes>[^\",]+)\")|" 27 r"(?P<value_no_quotes>[^'\", =]+)") 28 _END_OR_SEPARATOR_PATTERN = r"(\s*[,\s]\s*|$)" 29 _PARSE_RE = re.compile(fr"(?P<name>{_NAME_RE.pattern})" 30 fr"((?P<equal>=)({_VALUE_PATTERN})?)?" 31 fr"{_END_OR_SEPARATOR_PATTERN}") 32 33 @classmethod 34 def parse_str(cls, raw_flags: str) -> JSFlags: 35 return cls._parse_str(raw_flags, "--js-flags") 36 37 def copy(self) -> JSFlags: 38 return self.__class__(self) 39 40 def _set(self, 41 flag_name: str, 42 flag_value: Optional[str] = None, 43 override: bool = False) -> None: 44 self._validate_js_flag_name(flag_name) 45 if flag_value is not None: 46 self._validate_js_flag_value(flag_name, flag_value) 47 self._check_negated_flag(flag_name, override) 48 super()._set(flag_name, flag_value, override) 49 50 def _validate_js_flag_value(self, flag_name: str, flag_value: str) -> None: 51 if not isinstance(flag_value, str): 52 raise TypeError("JSFlag value must be str, " 53 f"but got {type(flag_value)}: {repr(flag_value)}") 54 if "," in flag_value: 55 raise ValueError( 56 "--js-flags: Comma in V8 flag value, flag escaping for chrome's " 57 f"--js-flags might not work: {flag_name}={repr(flag_value)}") 58 if self._WHITE_SPACE_RE.search(flag_value): 59 raise ValueError("--js-flags: V8 flag-values cannot contain whitespaces:" 60 f"{flag_name}={repr(flag_value)}") 61 62 def _validate_js_flag_name(self, flag_name: str) -> None: 63 if not flag_name.startswith("--"): 64 raise ValueError("--js-flags: Only long-form flag names allowed, " 65 f"but got {repr(flag_name)}") 66 if not self._NAME_RE.fullmatch(flag_name): 67 raise ValueError(f"--js-flags: Invalid flag name {repr(flag_name)}. \n" 68 "Check invalid characters in the V8 flag name?") 69 70 def _check_negated_flag(self, flag_name: str, override: bool) -> None: 71 if flag_name.startswith(self._NO_PREFIX): 72 enabled = flag_name[len(self._NO_PREFIX):] 73 # Check for --no-foo form 74 if enabled.startswith("-"): 75 enabled = enabled[1:] 76 enabled = "--" + enabled 77 if override: 78 del self[enabled] 79 elif enabled in self: 80 raise ValueError( 81 f"Conflicting flag {flag_name}, " 82 f"it has already been enabled by {repr(self._describe(enabled))}") 83 else: 84 # --foo => --no-foo 85 disabled = f"--no-{flag_name[2:]}" 86 if disabled not in self: 87 # Try compact version: --foo => --nofoo 88 disabled = f"--no{flag_name[2:]}" 89 if disabled not in self: 90 return 91 if override: 92 del self[disabled] 93 else: 94 raise ValueError(f"Conflicting flag {flag_name}, " 95 "it has previously been disabled by " 96 f"{repr(self._describe(flag_name))}") 97 98 def __str__(self) -> str: 99 return ",".join(self) 100