1#!/usr/bin/env python
2#
3# Copyright 2020 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6"""Helps generate enums.xml from ProductionSupportedFlagList.
7
8This is only a best-effort attempt to generate enums.xml values for the
9LoginCustomFlags enum. You need to verify this script picks the right string
10value for the new features and double check the hash value by running
11"AboutFlagsHistogramTest.*".
12"""
13
14from __future__ import print_function
15
16import argparse
17import os
18import re
19import hashlib
20import ctypes
21import xml.etree.ElementTree as ET
22import logging
23import sys
24
25_CHROMIUM_SRC = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)
26sys.path.append(os.path.join(_CHROMIUM_SRC, 'third_party', 'catapult', 'devil'))
27from devil.utils import logging_common  # pylint: disable=wrong-import-position
28
29_FLAG_LIST_FILE = os.path.join(_CHROMIUM_SRC, 'android_webview', 'java', 'src',
30                               'org', 'chromium', 'android_webview', 'common',
31                               'ProductionSupportedFlagList.java')
32_ENUMS_XML_FILE = os.path.join(_CHROMIUM_SRC, 'tools', 'metrics', 'histograms',
33                               'enums.xml')
34
35# This script tries to guess the commandline switch/base::Feature name from the
36# generated Java constant (assuming the constant name follows typical
37# conventions), but sometimes the script generates the incorrect name.
38# In this case, you can teach the
39# script the right name is by editing this dictionary. The perk of editing
40# here instead of fixing enums.xml by hand is this script *should* generate the
41# correct hash value once you add the right name, so you can just rerun the
42# script to get the correct set of enum entries.
43#
44# Keys are the names autogenerated by this script's logic, values are the
45# base::Feature/switch string names as they would appear in Java/C++ code.
46KNOWN_MISTAKES = {
47    # 'AutogeneratedName': 'CorrectName',
48    'WebViewAccelerateSmallCanvases': 'WebviewAccelerateSmallCanvases',
49    'EnableSharedImageForWebView': 'EnableSharedImageForWebview',
50}
51
52
53def GetSwitchId(label):
54  """Generate a hash consistent with flags_ui::GetSwitchUMAId()."""
55  digest = hashlib.md5(label).hexdigest()
56  first_eight_bytes = digest[:16]
57  long_value = int(first_eight_bytes, 16)
58  signed_32bit = ctypes.c_int(long_value).value
59  return signed_32bit
60
61
62def _Capitalize(value):
63  value = value[0].upper() + value[1:].lower()
64  if value == 'Webview':
65    value = 'WebView'
66  return value
67
68
69def FormatName(name, convert_to_pascal_case):
70  """Converts name to the correct format.
71
72  If name is shouty-case (ex. 'SOME_NAME') like a Java constant, then:
73    * it converts to pascal case (camel case, with the first letter capitalized)
74      if convert_to_pascal_case == True (ex. 'SomeName')
75    * it converts to hyphenates name and converts to lower case (ex.
76      'some-name')
77  raises
78    ValueError if name contains quotation marks like a Java literal (ex.
79      '"SomeName"')
80  """
81  has_quotes_re = re.compile(r'".*"')
82  if has_quotes_re.match(name):
83    raise ValueError('String literals are not supported (got {})'.format(name))
84  name = re.sub(r'^[^.]+\.', '', name)
85  sections = name.split('_')
86
87  if convert_to_pascal_case:
88    sections = [_Capitalize(section) for section in sections]
89    return ''.join(sections)
90
91  sections = [section.lower() for section in sections]
92  return '-'.join(sections)
93
94
95def ConvertNameIfNecessary(name):
96  """Fixes any names which are known to be autogenerated incorrectly."""
97  if name in KNOWN_MISTAKES.keys():
98    return KNOWN_MISTAKES.get(name)
99  return name
100
101
102class Flag(object):
103  """Simplified python equivalent of the Flag java class.
104
105  See //android_webview/java/src/org/chromium/android_webview/common/Flag.java
106  """
107
108  def __init__(self, name, is_base_feature):
109    self.name = name
110    self.is_base_feature = is_base_feature
111
112
113class EnumValue(object):
114  def __init__(self, label):
115    self.label = label
116    self.value = GetSwitchId(label)
117
118  def ToXml(self):
119    return '<int value="{value}" label="{label}"/>'.format(value=self.value,
120                                                           label=self.label)
121
122
123def _GetExistingFlagLabels():
124  with open(_ENUMS_XML_FILE) as f:
125    root = ET.fromstring(f.read())
126  all_enums = root.find('enums')
127  login_custom_flags = all_enums.find('enum[@name="LoginCustomFlags"]')
128  return [item.get('label') for item in login_custom_flags]
129
130
131def _RemoveDuplicates(enums, existing_labels):
132  return [enum for enum in enums if enum.label not in existing_labels]
133
134
135def ExtractFlagsFromJavaLines(lines):
136  flags = []
137
138  hanging_name_re = re.compile(
139      r'(?:\s*Flag\.(?:baseFeature|commandLine)\()?(\S+),')
140  pending_feature = False
141  pending_commandline = False
142
143  for line in lines:
144    if 'baseFeature(' in line:
145      pending_feature = True
146    if 'commandLine(' in line:
147      pending_commandline = True
148
149    if pending_feature and pending_commandline:
150      raise RuntimeError('Somehow this is both a baseFeature and commandLine '
151                         'switch: ({})'.format(line))
152
153    # This means we saw Flag.baseFeature() or Flag.commandLine() on this or a
154    # previous line but haven't found that flag's name yet. Check if we can
155    # find a name in this line.
156    if pending_feature or pending_commandline:
157      m = hanging_name_re.search(line)
158      if m:
159        name = m.group(1)
160        try:
161          formatted_name = FormatName(name, pending_feature)
162          formatted_name = ConvertNameIfNecessary(formatted_name)
163          flags.append(Flag(formatted_name, pending_feature))
164          pending_feature = False
165          pending_commandline = False
166        except ValueError:
167          logging.warning('String literals are not supported, skipping %s',
168                          name)
169  return flags
170
171
172def _GetMissingWebViewEnums():
173  with open(_FLAG_LIST_FILE, 'r') as f:
174    lines = f.readlines()
175  flags = ExtractFlagsFromJavaLines(lines)
176
177  enums = []
178  for flag in flags:
179    if flag.is_base_feature:
180      enums.append(EnumValue(flag.name + ':enabled'))
181      enums.append(EnumValue(flag.name + ':disabled'))
182    else:
183      enums.append(EnumValue(flag.name))
184
185  existing_labels = set(_GetExistingFlagLabels())
186  enums_to_add = _RemoveDuplicates(enums, existing_labels)
187  return enums_to_add
188
189
190def CheckMissingWebViewEnums(input_api, output_api):
191  """A presubmit check to find missing flag enums."""
192  sources = input_api.AffectedSourceFiles(
193      lambda affected_file: input_api.FilterSourceFile(
194          affected_file,
195          files_to_check=(r'.*\bProductionSupportedFlagList\.java$', )))
196  if not sources:
197    return []
198
199  enums_to_add = _GetMissingWebViewEnums()
200  if not enums_to_add:
201    return []
202
203  script_path = '//android_webview/tools/PRESUBMIT.py'
204  enums_path = '//tools/metrics/histograms/enums.xml'
205  xml_strs = sorted(['  ' + enum.ToXml() for enum in enums_to_add])
206
207  return [
208      output_api.PresubmitPromptWarning("""
209It looks like new flags have been added to ProductionSupportedFlagList but the
210labels still need to be added to LoginCustomFlags enum in {enums_path}.
211If you believe this
212warning is correct, please update enums.xml by pasting the following lines under
213LoginCustomFlags and running `git-cl format` to correctly sort the changes:
214
215{xml_strs}
216
217You can run this check again by running the {script_path} tool.
218
219If you believe this warning is a false positive, you can silence this warning by
220updating KNOWN_MISTAKES in {script_path}.
221""".format(xml_strs='\n'.join(xml_strs),
222           enums_path=enums_path,
223           script_path=script_path))
224  ]
225
226
227def main():
228  parser = argparse.ArgumentParser()
229
230  logging_common.AddLoggingArguments(parser)
231  args = parser.parse_args()
232  logging_common.InitializeLogging(args)
233
234  enums_to_add = _GetMissingWebViewEnums()
235
236  xml_strs = sorted(['  ' + enum.ToXml() for enum in enums_to_add])
237  if not xml_strs:
238    print('enums.xml is already up-to-date!')
239    return
240
241  message = """\
242This is a best-effort attempt to generate missing enums.xml entries. Please
243double-check this picked the correct labels for your new features (labels are
244case-sensitive!), add these to enums.xml, run `git-cl format`, and then follow
245these steps as a final check:
246
247https://chromium.googlesource.com/chromium/src/+/master/tools/metrics/histograms/README.md#flag-histograms
248
249If any labels were generated incorrectly, please edit this script and change
250KNOWN_MISTAKES.
251"""
252  print(message)
253
254  for xml_str in xml_strs:
255    print(xml_str)
256
257
258if __name__ == '__main__':
259  main()
260