• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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