1# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Presubmit script for ui/accessibility."""
6
7import os, re, json
8
9AX_MOJOM = 'ui/accessibility/ax_enums.mojom'
10AUTOMATION_IDL = 'extensions/common/api/automation.idl'
11
12AX_JS_FILE = 'chrome/browser/resources/accessibility/accessibility.js'
13AX_MODE_HEADER = 'ui/accessibility/ax_mode.h'
14
15def InitialLowerCamelCase(unix_name):
16  words = unix_name.split('_')
17  return words[0] + ''.join(word.capitalize() for word in words[1:])
18
19def CamelToLowerHacker(str):
20  out = ''
21  for i in range(len(str)):
22    if str[i] >= 'A' and str[i] <= 'Z' and out:
23      out += '_'
24    out += str[i]
25  return out.lower()
26
27# Given a full path to an IDL or MOJOM file containing enum definitions,
28# parse the file for enums and return a dict mapping the enum name
29# to a list of values for that enum.
30def GetEnumsFromFile(fullpath):
31  enum_name = None
32  enums = {}
33  for line in open(fullpath).readlines():
34    # Strip out comments
35    line = re.sub('//.*', '', line)
36
37    # Look for lines of the form "enum ENUM_NAME {" and get the enum_name
38    m = re.search('enum ([\w]+) {', line)
39    if m:
40      enum_name = m.group(1)
41      continue
42
43    # Look for a "}" character signifying the end of an enum
44    if line.find('}') >= 0:
45      enum_name = None
46      continue
47
48    if not enum_name:
49      continue
50
51    # If we're inside an enum definition, add the first string consisting of
52    # alphanumerics plus underscore ("\w") to the list of values for that enum.
53    m = re.search('([\w]+)', line)
54    if m:
55      enums.setdefault(enum_name, [])
56      enum_value = m.group(1)
57      if (enum_value[0] == 'k' and
58          enum_value[1] == enum_value[1].upper()):
59        enum_value = CamelToLowerHacker(enum_value[1:])
60      if enum_value == 'none' or enum_value == 'last':
61        continue
62      if enum_value == 'active_descendant_changed':
63        enum_value = 'activedescendantchanged'
64      enums[enum_name].append(enum_value)
65
66  return enums
67
68def CheckMatchingEnum(ax_enums,
69                      ax_enum_name,
70                      automation_enums,
71                      automation_enum_name,
72                      errs,
73                      output_api,
74                      strict_ordering=False):
75  if ax_enum_name not in ax_enums:
76    errs.append(output_api.PresubmitError(
77        'Expected %s to have an enum named %s' % (AX_MOJOM, ax_enum_name)))
78    return
79  if automation_enum_name not in automation_enums:
80    errs.append(output_api.PresubmitError(
81        'Expected %s to have an enum named %s' % (
82            AUTOMATION_IDL, automation_enum_name)))
83    return
84  src = ax_enums[ax_enum_name]
85  dst = automation_enums[automation_enum_name]
86  if strict_ordering and len(src) != len(dst):
87    errs.append(output_api.PresubmitError(
88        'Expected %s to have the same number of items as %s' % (
89            automation_enum_name, ax_enum_name)))
90    return
91
92  if strict_ordering:
93    for index, value in enumerate(src):
94      lower_value = InitialLowerCamelCase(value)
95      if lower_value != dst[index]:
96        errs.append(output_api.PresubmitError(
97            ('At index %s in enums, unexpected ordering around %s.%s ' +
98            'and %s.%s in %s and %s') % (
99                index, ax_enum_name, lower_value,
100                automation_enum_name, dst[index],
101                AX_MOJOM, AUTOMATION_IDL)))
102        return
103    return
104
105  for value in src:
106    lower_value = InitialLowerCamelCase(value)
107    if lower_value in dst:
108      dst.remove(lower_value)  # Any remaining at end are extra and a mismatch.
109    else:
110      errs.append(output_api.PresubmitError(
111          'Found %s.%s in %s, but did not find %s.%s in %s' % (
112              ax_enum_name, value, AX_MOJOM,
113              automation_enum_name, InitialLowerCamelCase(value),
114              AUTOMATION_IDL)))
115  #  Should be no remaining items
116  for value in dst:
117      errs.append(output_api.PresubmitError(
118          'Found %s.%s in %s, but did not find %s.%s in %s' % (
119              automation_enum_name, value, AUTOMATION_IDL,
120              ax_enum_name, InitialLowerCamelCase(value),
121              AX_MOJOM)))
122
123def CheckEnumsMatch(input_api, output_api):
124  repo_root = input_api.change.RepositoryRoot()
125  ax_enums = GetEnumsFromFile(os.path.join(repo_root, AX_MOJOM))
126  automation_enums = GetEnumsFromFile(os.path.join(repo_root, AUTOMATION_IDL))
127
128  # Focused state only exists in automation.
129  automation_enums['StateType'].remove('focused')
130  # Offscreen state only exists in automation.
131  automation_enums['StateType'].remove('offscreen')
132
133  errs = []
134  CheckMatchingEnum(ax_enums, 'Role', automation_enums, 'RoleType', errs,
135                    output_api)
136  CheckMatchingEnum(ax_enums, 'State', automation_enums, 'StateType', errs,
137                    output_api, strict_ordering=True)
138  CheckMatchingEnum(ax_enums, 'Action', automation_enums, 'ActionType', errs,
139                    output_api, strict_ordering=True)
140  CheckMatchingEnum(ax_enums, 'Event', automation_enums, 'EventType', errs,
141                    output_api)
142  CheckMatchingEnum(ax_enums, 'NameFrom', automation_enums, 'NameFromType',
143                    errs, output_api)
144  CheckMatchingEnum(ax_enums, 'DescriptionFrom', automation_enums,
145                    'DescriptionFromType', errs, output_api)
146  CheckMatchingEnum(ax_enums, 'Restriction', automation_enums,
147                   'Restriction', errs, output_api)
148  CheckMatchingEnum(ax_enums, 'DefaultActionVerb', automation_enums,
149                   'DefaultActionVerb', errs, output_api)
150  CheckMatchingEnum(ax_enums, 'MarkerType', automation_enums,
151                   'MarkerType', errs, output_api)
152  return errs
153
154# Given a full path to c++ header, return an array of the first static
155# constexpr defined. (Note there can be more than one defined in a C++
156# header)
157def GetConstexprFromFile(fullpath):
158  values = []
159  for line in open(fullpath).readlines():
160    # Strip out comments
161    line = re.sub('//.*', '', line)
162
163    # Look for lines of the form "static constexpr <type> NAME "
164    m = re.search('static constexpr [\w]+ ([\w]+)', line)
165    if m:
166      value = m.group(1)
167      # Skip first/last sentinels
168      if value == 'kFirstModeFlag' or value == 'kLastModeFlag':
169        continue
170      values.append(value)
171
172  return values
173
174# Given a full path to js file, return the AXMode consts
175# defined
176def GetAccessibilityModesFromFile(fullpath):
177  values = []
178  inside = False
179  for line in open(fullpath).readlines():
180    # Strip out comments
181    line = re.sub('//.*', '', line)
182
183    # Look for the block of code that defines AXMode
184    m = re.search('const AXMode = {', line)
185    if m:
186      inside = True
187      continue
188
189    # Look for a "}" character signifying the end of an enum
190    if line.find('};') >= 0:
191      return values
192
193    if not inside:
194      continue
195
196    m = re.search('([\w]+):', line)
197    if m:
198      values.append(m.group(1))
199      continue
200
201    # getters
202    m = re.search('get ([\w]+)\(\)', line)
203    if m:
204      values.append(m.group(1))
205  return values
206
207# Make sure that the modes defined in the C++ header match those defined in
208# the js file. Note that this doesn't guarantee that the values are the same,
209# but does make sure if we add or remove we can signal to the developer that
210# they should be aware that this dependency exists.
211def CheckModesMatch(input_api, output_api):
212  errs = []
213  repo_root = input_api.change.RepositoryRoot()
214
215  ax_modes_in_header = GetConstexprFromFile(
216    os.path.join(repo_root,AX_MODE_HEADER))
217  ax_modes_in_js = GetAccessibilityModesFromFile(
218    os.path.join(repo_root, AX_JS_FILE))
219
220  for value in ax_modes_in_header:
221    if value not in ax_modes_in_js:
222      errs.append(output_api.PresubmitError(
223          'Found %s in %s, but did not find %s in %s' % (
224              value, AX_MODE_HEADER, value, AX_JS_FILE)))
225  return errs
226
227def CheckChangeOnUpload(input_api, output_api):
228  errs = []
229  for path in input_api.LocalPaths():
230    path = path.replace('\\', '/')
231    if AX_MOJOM == path:
232      errs.extend(CheckEnumsMatch(input_api, output_api))
233
234    if AX_MODE_HEADER == path:
235      errs.extend(CheckModesMatch(input_api, output_api))
236
237  return errs
238
239def CheckChangeOnCommit(input_api, output_api):
240  errs = []
241  for path in input_api.LocalPaths():
242    path = path.replace('\\', '/')
243    if AX_MOJOM == path:
244      errs.extend(CheckEnumsMatch(input_api, output_api))
245
246    if AX_MODE_HEADER == path:
247      errs.extend(CheckModesMatch(input_api, output_api))
248
249  return errs
250