xref: /aosp_15_r20/external/perfetto/python/tools/check_imports.py (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker#!/usr/bin/env python3
2*6dbdd20aSAndroid Build Coastguard Worker# Copyright (C) 2023 The Android Open Source Project
3*6dbdd20aSAndroid Build Coastguard Worker#
4*6dbdd20aSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
5*6dbdd20aSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
6*6dbdd20aSAndroid Build Coastguard Worker# You may obtain a copy of the License at
7*6dbdd20aSAndroid Build Coastguard Worker#
8*6dbdd20aSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
9*6dbdd20aSAndroid Build Coastguard Worker#
10*6dbdd20aSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
11*6dbdd20aSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
12*6dbdd20aSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13*6dbdd20aSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
14*6dbdd20aSAndroid Build Coastguard Worker# limitations under the License.
15*6dbdd20aSAndroid Build Coastguard Worker"""
16*6dbdd20aSAndroid Build Coastguard WorkerEnforce import rules for https://ui.perfetto.dev.
17*6dbdd20aSAndroid Build Coastguard Worker"""
18*6dbdd20aSAndroid Build Coastguard Worker
19*6dbdd20aSAndroid Build Coastguard Workerimport sys
20*6dbdd20aSAndroid Build Coastguard Workerimport os
21*6dbdd20aSAndroid Build Coastguard Workerimport re
22*6dbdd20aSAndroid Build Coastguard Workerimport collections
23*6dbdd20aSAndroid Build Coastguard Workerimport argparse
24*6dbdd20aSAndroid Build Coastguard Workerimport fnmatch
25*6dbdd20aSAndroid Build Coastguard Worker
26*6dbdd20aSAndroid Build Coastguard WorkerROOT_DIR = os.path.dirname(
27*6dbdd20aSAndroid Build Coastguard Worker    os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
28*6dbdd20aSAndroid Build Coastguard WorkerUI_SRC_DIR = os.path.join(ROOT_DIR, 'ui', 'src')
29*6dbdd20aSAndroid Build Coastguard Worker
30*6dbdd20aSAndroid Build Coastguard WorkerNODE_MODULES = '%node_modules%'  # placeholder to depend on any node module.
31*6dbdd20aSAndroid Build Coastguard Worker
32*6dbdd20aSAndroid Build Coastguard Worker# The format of this array is: (src) -> (dst).
33*6dbdd20aSAndroid Build Coastguard Worker# If src or dst are arrays, the semantic is the cartesian product, e.g.:
34*6dbdd20aSAndroid Build Coastguard Worker# [a,b] -> [c,d] is equivalent to allowing a>c, a>d, b>c, b>d.
35*6dbdd20aSAndroid Build Coastguard WorkerDEPS_ALLOWLIST = [
36*6dbdd20aSAndroid Build Coastguard Worker    # Everything can depend on base/, protos and NPM packages.
37*6dbdd20aSAndroid Build Coastguard Worker    ('*', ['/base/*', '/protos/index', '/gen/perfetto_version', NODE_MODULES]),
38*6dbdd20aSAndroid Build Coastguard Worker
39*6dbdd20aSAndroid Build Coastguard Worker    # Integration tests can depend on everything.
40*6dbdd20aSAndroid Build Coastguard Worker    ('/test/*', '*'),
41*6dbdd20aSAndroid Build Coastguard Worker
42*6dbdd20aSAndroid Build Coastguard Worker    # Dependencies allowed for internal UI code.
43*6dbdd20aSAndroid Build Coastguard Worker    (
44*6dbdd20aSAndroid Build Coastguard Worker        [
45*6dbdd20aSAndroid Build Coastguard Worker            '/frontend/*',
46*6dbdd20aSAndroid Build Coastguard Worker            '/core/*',
47*6dbdd20aSAndroid Build Coastguard Worker            '/common/*',
48*6dbdd20aSAndroid Build Coastguard Worker        ],
49*6dbdd20aSAndroid Build Coastguard Worker        [
50*6dbdd20aSAndroid Build Coastguard Worker            '/frontend/*',
51*6dbdd20aSAndroid Build Coastguard Worker            '/core/*',
52*6dbdd20aSAndroid Build Coastguard Worker            '/common/*',
53*6dbdd20aSAndroid Build Coastguard Worker            '/components/*',
54*6dbdd20aSAndroid Build Coastguard Worker            '/public/*',
55*6dbdd20aSAndroid Build Coastguard Worker            '/trace_processor/*',
56*6dbdd20aSAndroid Build Coastguard Worker            '/widgets/*',
57*6dbdd20aSAndroid Build Coastguard Worker            '/protos/*',
58*6dbdd20aSAndroid Build Coastguard Worker            '/gen/perfetto_version',
59*6dbdd20aSAndroid Build Coastguard Worker        ],
60*6dbdd20aSAndroid Build Coastguard Worker    ),
61*6dbdd20aSAndroid Build Coastguard Worker
62*6dbdd20aSAndroid Build Coastguard Worker    # /public (interfaces + lib) can depend only on a restricted surface.
63*6dbdd20aSAndroid Build Coastguard Worker    ('/public/*', ['/base/*', '/trace_processor/*', '/widgets/*']),
64*6dbdd20aSAndroid Build Coastguard Worker
65*6dbdd20aSAndroid Build Coastguard Worker    # /plugins (and core_plugins) can depend only on a restricted surface.
66*6dbdd20aSAndroid Build Coastguard Worker    (
67*6dbdd20aSAndroid Build Coastguard Worker        '/*plugins/*',
68*6dbdd20aSAndroid Build Coastguard Worker        [
69*6dbdd20aSAndroid Build Coastguard Worker            '/base/*', '/public/*', '/trace_processor/*', '/widgets/*',
70*6dbdd20aSAndroid Build Coastguard Worker            '/components/*'
71*6dbdd20aSAndroid Build Coastguard Worker        ],
72*6dbdd20aSAndroid Build Coastguard Worker    ),
73*6dbdd20aSAndroid Build Coastguard Worker
74*6dbdd20aSAndroid Build Coastguard Worker    # /components can depend on the 'base' packages & public
75*6dbdd20aSAndroid Build Coastguard Worker    (
76*6dbdd20aSAndroid Build Coastguard Worker        '/components/*',
77*6dbdd20aSAndroid Build Coastguard Worker        [
78*6dbdd20aSAndroid Build Coastguard Worker            '/base/*',
79*6dbdd20aSAndroid Build Coastguard Worker            '/trace_processor/*',
80*6dbdd20aSAndroid Build Coastguard Worker            '/widgets/*',
81*6dbdd20aSAndroid Build Coastguard Worker            '/public/*',
82*6dbdd20aSAndroid Build Coastguard Worker            '/components/*',
83*6dbdd20aSAndroid Build Coastguard Worker        ],
84*6dbdd20aSAndroid Build Coastguard Worker    ),
85*6dbdd20aSAndroid Build Coastguard Worker
86*6dbdd20aSAndroid Build Coastguard Worker    # Extra dependencies allowed for core_plugins only.
87*6dbdd20aSAndroid Build Coastguard Worker    # TODO(priniano): remove this entry to figure out what it takes to move the
88*6dbdd20aSAndroid Build Coastguard Worker    # remaining /core_plugins to /plugins and get rid of core_plugins.
89*6dbdd20aSAndroid Build Coastguard Worker    (
90*6dbdd20aSAndroid Build Coastguard Worker        ['/core_plugins/*'],
91*6dbdd20aSAndroid Build Coastguard Worker        ['/core/*', '/frontend/*', '/common/actions'],
92*6dbdd20aSAndroid Build Coastguard Worker    ),
93*6dbdd20aSAndroid Build Coastguard Worker
94*6dbdd20aSAndroid Build Coastguard Worker    # Miscl legitimate deps.
95*6dbdd20aSAndroid Build Coastguard Worker    ('/frontend/index', ['/gen/*']),
96*6dbdd20aSAndroid Build Coastguard Worker    ('/traceconv/index', '/gen/traceconv'),
97*6dbdd20aSAndroid Build Coastguard Worker    ('/engine/wasm_bridge', '/gen/trace_processor'),
98*6dbdd20aSAndroid Build Coastguard Worker    ('/trace_processor/sql_utils/*', '/trace_processor/*'),
99*6dbdd20aSAndroid Build Coastguard Worker    ('/protos/index', '/gen/protos'),
100*6dbdd20aSAndroid Build Coastguard Worker
101*6dbdd20aSAndroid Build Coastguard Worker    # ------ Technical debt that needs cleaning up below this point ------
102*6dbdd20aSAndroid Build Coastguard Worker    # TODO(stevegolton): Remove these once we extract core types out of
103*6dbdd20aSAndroid Build Coastguard Worker    # components.
104*6dbdd20aSAndroid Build Coastguard Worker    (
105*6dbdd20aSAndroid Build Coastguard Worker        '/components/*',
106*6dbdd20aSAndroid Build Coastguard Worker        [
107*6dbdd20aSAndroid Build Coastguard Worker            '/core/trace_impl',
108*6dbdd20aSAndroid Build Coastguard Worker            '/core/app_impl',
109*6dbdd20aSAndroid Build Coastguard Worker            '/core/router',
110*6dbdd20aSAndroid Build Coastguard Worker            '/core/flow_types',
111*6dbdd20aSAndroid Build Coastguard Worker            '/core/fake_trace_impl',
112*6dbdd20aSAndroid Build Coastguard Worker            '/core/raf_scheduler',
113*6dbdd20aSAndroid Build Coastguard Worker            '/core/feature_flags',
114*6dbdd20aSAndroid Build Coastguard Worker            '/frontend/css_constants',
115*6dbdd20aSAndroid Build Coastguard Worker        ],
116*6dbdd20aSAndroid Build Coastguard Worker    ),
117*6dbdd20aSAndroid Build Coastguard Worker
118*6dbdd20aSAndroid Build Coastguard Worker    # TODO(primiano): Record page-related technical debt.
119*6dbdd20aSAndroid Build Coastguard Worker    ('/plugins/dev.perfetto.RecordTrace/*',
120*6dbdd20aSAndroid Build Coastguard Worker     ['/frontend/globals', '/gen/protos']),
121*6dbdd20aSAndroid Build Coastguard Worker    ('/chrome_extension/chrome_tracing_controller',
122*6dbdd20aSAndroid Build Coastguard Worker     '/plugins/dev.perfetto.RecordTrace/*'),
123*6dbdd20aSAndroid Build Coastguard Worker
124*6dbdd20aSAndroid Build Coastguard Worker    # Bigtrace deps.
125*6dbdd20aSAndroid Build Coastguard Worker    ('/bigtrace/*', ['/base/*', '/widgets/*', '/trace_processor/*']),
126*6dbdd20aSAndroid Build Coastguard Worker
127*6dbdd20aSAndroid Build Coastguard Worker    # TODO(primiano): misc tech debt.
128*6dbdd20aSAndroid Build Coastguard Worker    ('/public/lib/extensions', '/frontend/*'),
129*6dbdd20aSAndroid Build Coastguard Worker    ('/bigtrace/index', ['/core/live_reload', '/core/raf_scheduler']),
130*6dbdd20aSAndroid Build Coastguard Worker    ('/plugins/dev.perfetto.HeapProfile/*', '/frontend/trace_converter'),
131*6dbdd20aSAndroid Build Coastguard Worker]
132*6dbdd20aSAndroid Build Coastguard Worker
133*6dbdd20aSAndroid Build Coastguard Worker
134*6dbdd20aSAndroid Build Coastguard Workerdef all_source_files():
135*6dbdd20aSAndroid Build Coastguard Worker  for root, dirs, files in os.walk(UI_SRC_DIR, followlinks=False):
136*6dbdd20aSAndroid Build Coastguard Worker    for name in files:
137*6dbdd20aSAndroid Build Coastguard Worker      if name.endswith('.ts') and not name.endswith('.d.ts'):
138*6dbdd20aSAndroid Build Coastguard Worker        yield os.path.join(root, name)
139*6dbdd20aSAndroid Build Coastguard Worker
140*6dbdd20aSAndroid Build Coastguard Worker
141*6dbdd20aSAndroid Build Coastguard Workerdef is_dir(path, cache={}):
142*6dbdd20aSAndroid Build Coastguard Worker  try:
143*6dbdd20aSAndroid Build Coastguard Worker    return cache[path]
144*6dbdd20aSAndroid Build Coastguard Worker  except KeyError:
145*6dbdd20aSAndroid Build Coastguard Worker    result = cache[path] = os.path.isdir(path)
146*6dbdd20aSAndroid Build Coastguard Worker    return result
147*6dbdd20aSAndroid Build Coastguard Worker
148*6dbdd20aSAndroid Build Coastguard Worker
149*6dbdd20aSAndroid Build Coastguard Workerdef remove_prefix(s, prefix):
150*6dbdd20aSAndroid Build Coastguard Worker  return s[len(prefix):] if s.startswith(prefix) else s
151*6dbdd20aSAndroid Build Coastguard Worker
152*6dbdd20aSAndroid Build Coastguard Worker
153*6dbdd20aSAndroid Build Coastguard Workerdef remove_suffix(s, suffix):
154*6dbdd20aSAndroid Build Coastguard Worker  return s[:-len(suffix)] if s.endswith(suffix) else s
155*6dbdd20aSAndroid Build Coastguard Worker
156*6dbdd20aSAndroid Build Coastguard Worker
157*6dbdd20aSAndroid Build Coastguard Workerdef normalize_path(path):
158*6dbdd20aSAndroid Build Coastguard Worker  return remove_suffix(remove_prefix(path, UI_SRC_DIR), '.ts')
159*6dbdd20aSAndroid Build Coastguard Worker
160*6dbdd20aSAndroid Build Coastguard Worker
161*6dbdd20aSAndroid Build Coastguard Workerdef find_plugin_declared_deps(path):
162*6dbdd20aSAndroid Build Coastguard Worker  """Returns the set of deps declared by the plugin (if any)
163*6dbdd20aSAndroid Build Coastguard Worker
164*6dbdd20aSAndroid Build Coastguard Worker  It scans the plugin/index.ts file, and resolves the declared dependencies,
165*6dbdd20aSAndroid Build Coastguard Worker  working out the path of the plugin we depend on (by looking at the imports).
166*6dbdd20aSAndroid Build Coastguard Worker  Returns a tuple of the form (src_plugin_path, set{dst_plugin_path})
167*6dbdd20aSAndroid Build Coastguard Worker  Where:
168*6dbdd20aSAndroid Build Coastguard Worker    src_plugin_path: is the normalized path of the input (e.g. /plugins/foo)
169*6dbdd20aSAndroid Build Coastguard Worker    dst_path: is the normalized path of the declared dependency.
170*6dbdd20aSAndroid Build Coastguard Worker  """
171*6dbdd20aSAndroid Build Coastguard Worker  src = normalize_path(path)
172*6dbdd20aSAndroid Build Coastguard Worker  src_plugin = get_plugin_path(src)
173*6dbdd20aSAndroid Build Coastguard Worker  if src_plugin is None or src != src_plugin + '/index':
174*6dbdd20aSAndroid Build Coastguard Worker    # If the file is not a plugin, or is not the plugin index.ts, bail out.
175*6dbdd20aSAndroid Build Coastguard Worker    return
176*6dbdd20aSAndroid Build Coastguard Worker  # First extract all the default-imports in the file. Usually there is one for
177*6dbdd20aSAndroid Build Coastguard Worker  # each imported plugin, of the form:
178*6dbdd20aSAndroid Build Coastguard Worker  # import ThreadPlugin from '../plugins/dev.perfetto.Thread'
179*6dbdd20aSAndroid Build Coastguard Worker  import_map = {}  # 'ThreadPlugin' -> '/plugins/dev.perfetto.Thread'
180*6dbdd20aSAndroid Build Coastguard Worker  for (src, target, default_import) in find_imports(path):
181*6dbdd20aSAndroid Build Coastguard Worker    target_plugin = get_plugin_path(target)
182*6dbdd20aSAndroid Build Coastguard Worker    if default_import is not None or target_plugin is not None:
183*6dbdd20aSAndroid Build Coastguard Worker      import_map[default_import] = target_plugin
184*6dbdd20aSAndroid Build Coastguard Worker
185*6dbdd20aSAndroid Build Coastguard Worker  # Now extract the declared dependencies for the plugin. This looks for the
186*6dbdd20aSAndroid Build Coastguard Worker  # statement 'static readonly dependencies = [ThreadPlugin]'. It can be broken
187*6dbdd20aSAndroid Build Coastguard Worker  # down over multiple lines, so we approach this in two steps. First we find
188*6dbdd20aSAndroid Build Coastguard Worker  # everything within the square brackets; then we remove spaces and \n and
189*6dbdd20aSAndroid Build Coastguard Worker  # tokenize on commas
190*6dbdd20aSAndroid Build Coastguard Worker  with open(path) as f:
191*6dbdd20aSAndroid Build Coastguard Worker    s = f.read()
192*6dbdd20aSAndroid Build Coastguard Worker  DEP_REGEX = r'^\s*static readonly dependencies\s*=\s*\[([^\]]*)\]'
193*6dbdd20aSAndroid Build Coastguard Worker  all_deps = re.findall(DEP_REGEX, s, flags=re.MULTILINE)
194*6dbdd20aSAndroid Build Coastguard Worker  if len(all_deps) == 0:
195*6dbdd20aSAndroid Build Coastguard Worker    return
196*6dbdd20aSAndroid Build Coastguard Worker  if len(all_deps) > 1:
197*6dbdd20aSAndroid Build Coastguard Worker    raise Exception('Ambiguous plugin deps in %s: %s' % (path, all_deps))
198*6dbdd20aSAndroid Build Coastguard Worker  declared_deps = re.sub('\s*', '', all_deps[0]).split(',')
199*6dbdd20aSAndroid Build Coastguard Worker  for imported_as in declared_deps:
200*6dbdd20aSAndroid Build Coastguard Worker    resolved_dep = import_map.get(imported_as)
201*6dbdd20aSAndroid Build Coastguard Worker    if resolved_dep is None:
202*6dbdd20aSAndroid Build Coastguard Worker      raise Exception('Could not resolve import %s in %s' % (imported_as, src))
203*6dbdd20aSAndroid Build Coastguard Worker    yield (src_plugin, resolved_dep)
204*6dbdd20aSAndroid Build Coastguard Worker
205*6dbdd20aSAndroid Build Coastguard Worker
206*6dbdd20aSAndroid Build Coastguard Workerdef find_imports(path):
207*6dbdd20aSAndroid Build Coastguard Worker  src = normalize_path(path)
208*6dbdd20aSAndroid Build Coastguard Worker  directory, _ = os.path.split(src)
209*6dbdd20aSAndroid Build Coastguard Worker  with open(path) as f:
210*6dbdd20aSAndroid Build Coastguard Worker    s = f.read()
211*6dbdd20aSAndroid Build Coastguard Worker  for m in re.finditer(
212*6dbdd20aSAndroid Build Coastguard Worker      "^import\s+([^;]+)\s+from\s+'([^']+)';$", s, flags=re.MULTILINE):
213*6dbdd20aSAndroid Build Coastguard Worker    # Flatten multi-line imports into one line, removing spaces. The resulting
214*6dbdd20aSAndroid Build Coastguard Worker    # import line can look like:
215*6dbdd20aSAndroid Build Coastguard Worker    # '{foo,bar,baz}' in most cases
216*6dbdd20aSAndroid Build Coastguard Worker    # 'DefaultImportName' when doing import DefaultImportName from '...'
217*6dbdd20aSAndroid Build Coastguard Worker    # 'DefaultImportName,{foo,bar,bar}' when doing a mixture of the above.
218*6dbdd20aSAndroid Build Coastguard Worker    imports = re.sub('\s', '', m[1])
219*6dbdd20aSAndroid Build Coastguard Worker    default_import = (re.findall('^\w+', imports) + [None])[0]
220*6dbdd20aSAndroid Build Coastguard Worker
221*6dbdd20aSAndroid Build Coastguard Worker    # Normalize the imported file
222*6dbdd20aSAndroid Build Coastguard Worker    target = m[2]
223*6dbdd20aSAndroid Build Coastguard Worker    if target.startswith('.'):
224*6dbdd20aSAndroid Build Coastguard Worker      target = os.path.normpath(os.path.join(directory, target))
225*6dbdd20aSAndroid Build Coastguard Worker      if is_dir(UI_SRC_DIR + target):
226*6dbdd20aSAndroid Build Coastguard Worker        target = os.path.join(target, 'index')
227*6dbdd20aSAndroid Build Coastguard Worker
228*6dbdd20aSAndroid Build Coastguard Worker    yield (src, target, default_import)
229*6dbdd20aSAndroid Build Coastguard Worker
230*6dbdd20aSAndroid Build Coastguard Worker
231*6dbdd20aSAndroid Build Coastguard Workerdef path_to_id(path):
232*6dbdd20aSAndroid Build Coastguard Worker  path = path.replace('/', '_')
233*6dbdd20aSAndroid Build Coastguard Worker  path = path.replace('-', '_')
234*6dbdd20aSAndroid Build Coastguard Worker  path = path.replace('@', '_at_')
235*6dbdd20aSAndroid Build Coastguard Worker  path = path.replace('.', '_')
236*6dbdd20aSAndroid Build Coastguard Worker  return path
237*6dbdd20aSAndroid Build Coastguard Worker
238*6dbdd20aSAndroid Build Coastguard Worker
239*6dbdd20aSAndroid Build Coastguard Workerdef is_external_dep(path):
240*6dbdd20aSAndroid Build Coastguard Worker  return not path.startswith('/')
241*6dbdd20aSAndroid Build Coastguard Worker
242*6dbdd20aSAndroid Build Coastguard Worker
243*6dbdd20aSAndroid Build Coastguard Workerdef write_dot(graph, f):
244*6dbdd20aSAndroid Build Coastguard Worker  print('digraph g {', file=f)
245*6dbdd20aSAndroid Build Coastguard Worker  for node, edges in graph.items():
246*6dbdd20aSAndroid Build Coastguard Worker    node_id = path_to_id(node)
247*6dbdd20aSAndroid Build Coastguard Worker    shape = 'rectangle' if is_external_dep(node) else 'ellipse'
248*6dbdd20aSAndroid Build Coastguard Worker    print(f'{node_id} [shape={shape}, label="{node}"];', file=f)
249*6dbdd20aSAndroid Build Coastguard Worker
250*6dbdd20aSAndroid Build Coastguard Worker    for edge in edges:
251*6dbdd20aSAndroid Build Coastguard Worker      edge_id = path_to_id(edge)
252*6dbdd20aSAndroid Build Coastguard Worker      print(f'{node_id} -> {edge_id};', file=f)
253*6dbdd20aSAndroid Build Coastguard Worker  print('}', file=f)
254*6dbdd20aSAndroid Build Coastguard Worker
255*6dbdd20aSAndroid Build Coastguard Worker
256*6dbdd20aSAndroid Build Coastguard Workerdef get_plugin_path(path):
257*6dbdd20aSAndroid Build Coastguard Worker  m = re.match('^(/(?:core_)?plugins/([^/]+))/.*', path)
258*6dbdd20aSAndroid Build Coastguard Worker  return m.group(1) if m is not None else None
259*6dbdd20aSAndroid Build Coastguard Worker
260*6dbdd20aSAndroid Build Coastguard Worker
261*6dbdd20aSAndroid Build Coastguard Workerdef flatten_rules(rules):
262*6dbdd20aSAndroid Build Coastguard Worker  flat_deps = []
263*6dbdd20aSAndroid Build Coastguard Worker  for rule_src, rule_dst in rules:
264*6dbdd20aSAndroid Build Coastguard Worker    src_list = rule_src if isinstance(rule_src, list) else [rule_src]
265*6dbdd20aSAndroid Build Coastguard Worker    dst_list = rule_dst if isinstance(rule_dst, list) else [rule_dst]
266*6dbdd20aSAndroid Build Coastguard Worker    for src in src_list:
267*6dbdd20aSAndroid Build Coastguard Worker      for dst in dst_list:
268*6dbdd20aSAndroid Build Coastguard Worker        flat_deps.append((src, dst))
269*6dbdd20aSAndroid Build Coastguard Worker  return flat_deps
270*6dbdd20aSAndroid Build Coastguard Worker
271*6dbdd20aSAndroid Build Coastguard Worker
272*6dbdd20aSAndroid Build Coastguard Workerdef get_node_modules(graph):
273*6dbdd20aSAndroid Build Coastguard Worker  """Infers the dependencies onto NPM packages (node_modules)
274*6dbdd20aSAndroid Build Coastguard Worker
275*6dbdd20aSAndroid Build Coastguard Worker  An import is guessed to be a node module if doesn't contain any . or .. in the
276*6dbdd20aSAndroid Build Coastguard Worker  path, and optionally starts with @.
277*6dbdd20aSAndroid Build Coastguard Worker  """
278*6dbdd20aSAndroid Build Coastguard Worker  node_modules = set()
279*6dbdd20aSAndroid Build Coastguard Worker  for _, imports in graph.items():
280*6dbdd20aSAndroid Build Coastguard Worker    for dst in imports:
281*6dbdd20aSAndroid Build Coastguard Worker      if re.match(r'^[@a-z][a-z0-9-_/]+$', dst):
282*6dbdd20aSAndroid Build Coastguard Worker        node_modules.add(dst)
283*6dbdd20aSAndroid Build Coastguard Worker  return node_modules
284*6dbdd20aSAndroid Build Coastguard Worker
285*6dbdd20aSAndroid Build Coastguard Worker
286*6dbdd20aSAndroid Build Coastguard Workerdef check_one_import(src, dst, allowlist, plugin_declared_deps, node_modules):
287*6dbdd20aSAndroid Build Coastguard Worker  # Translate node_module deps into the wildcard '%node_modules%' so it can be
288*6dbdd20aSAndroid Build Coastguard Worker  # treated as a single entity.
289*6dbdd20aSAndroid Build Coastguard Worker  if dst in node_modules:
290*6dbdd20aSAndroid Build Coastguard Worker    dst = NODE_MODULES
291*6dbdd20aSAndroid Build Coastguard Worker
292*6dbdd20aSAndroid Build Coastguard Worker  # Always allow imports from the same directory or its own subdirectories.
293*6dbdd20aSAndroid Build Coastguard Worker  src_dir = '/'.join(src.split('/')[:-1])
294*6dbdd20aSAndroid Build Coastguard Worker  dst_dir = '/'.join(dst.split('/')[:-1])
295*6dbdd20aSAndroid Build Coastguard Worker  if dst_dir.startswith(src_dir):
296*6dbdd20aSAndroid Build Coastguard Worker    return True
297*6dbdd20aSAndroid Build Coastguard Worker
298*6dbdd20aSAndroid Build Coastguard Worker  # Match against the (flattened) allowlist.
299*6dbdd20aSAndroid Build Coastguard Worker  for rule_src, rule_dst in allowlist:
300*6dbdd20aSAndroid Build Coastguard Worker    if fnmatch.fnmatch(src, rule_src) and fnmatch.fnmatch(dst, rule_dst):
301*6dbdd20aSAndroid Build Coastguard Worker      return True
302*6dbdd20aSAndroid Build Coastguard Worker
303*6dbdd20aSAndroid Build Coastguard Worker  # Check inter-plugin deps.
304*6dbdd20aSAndroid Build Coastguard Worker  src_plugin = get_plugin_path(src)
305*6dbdd20aSAndroid Build Coastguard Worker  dst_plugin = get_plugin_path(dst)
306*6dbdd20aSAndroid Build Coastguard Worker  extra_err = ''
307*6dbdd20aSAndroid Build Coastguard Worker  if src_plugin is not None and dst_plugin is not None:
308*6dbdd20aSAndroid Build Coastguard Worker    if src_plugin == dst_plugin:
309*6dbdd20aSAndroid Build Coastguard Worker      # Allow a plugin to depends on arbitrary subdirectories of itself.
310*6dbdd20aSAndroid Build Coastguard Worker      return True
311*6dbdd20aSAndroid Build Coastguard Worker    # Check if there is a dependency declared by plugins, via
312*6dbdd20aSAndroid Build Coastguard Worker    # static readonly dependencies = [DstPlugin]
313*6dbdd20aSAndroid Build Coastguard Worker    declared_deps = plugin_declared_deps.get(src_plugin, set())
314*6dbdd20aSAndroid Build Coastguard Worker    extra_err = '(plugin deps: %s)' % ','.join(declared_deps)
315*6dbdd20aSAndroid Build Coastguard Worker    if dst_plugin in declared_deps:
316*6dbdd20aSAndroid Build Coastguard Worker      return True
317*6dbdd20aSAndroid Build Coastguard Worker  print('Import not allowed %s -> %s %s' % (src, dst, extra_err))
318*6dbdd20aSAndroid Build Coastguard Worker  return False
319*6dbdd20aSAndroid Build Coastguard Worker
320*6dbdd20aSAndroid Build Coastguard Worker
321*6dbdd20aSAndroid Build Coastguard Workerdef do_check(_options, graph):
322*6dbdd20aSAndroid Build Coastguard Worker  result = 0
323*6dbdd20aSAndroid Build Coastguard Worker  rules = flatten_rules(DEPS_ALLOWLIST)
324*6dbdd20aSAndroid Build Coastguard Worker  node_modules = get_node_modules(graph)
325*6dbdd20aSAndroid Build Coastguard Worker
326*6dbdd20aSAndroid Build Coastguard Worker  # Build a map of depencies declared between plugin. The maps looks like:
327*6dbdd20aSAndroid Build Coastguard Worker  # 'Foo' -> {'Bar', 'Baz'}  # Foo declares a dependency on Bar and Baz
328*6dbdd20aSAndroid Build Coastguard Worker  plugin_declared_deps = collections.defaultdict(set)
329*6dbdd20aSAndroid Build Coastguard Worker  for path in all_source_files():
330*6dbdd20aSAndroid Build Coastguard Worker    for src_plugin, dst_plugin in find_plugin_declared_deps(path):
331*6dbdd20aSAndroid Build Coastguard Worker      plugin_declared_deps[src_plugin].add(dst_plugin)
332*6dbdd20aSAndroid Build Coastguard Worker
333*6dbdd20aSAndroid Build Coastguard Worker  for src, imports in graph.items():
334*6dbdd20aSAndroid Build Coastguard Worker    for dst in imports:
335*6dbdd20aSAndroid Build Coastguard Worker      if not check_one_import(src, dst, rules, plugin_declared_deps,
336*6dbdd20aSAndroid Build Coastguard Worker                              node_modules):
337*6dbdd20aSAndroid Build Coastguard Worker        result = 1
338*6dbdd20aSAndroid Build Coastguard Worker  return result
339*6dbdd20aSAndroid Build Coastguard Worker
340*6dbdd20aSAndroid Build Coastguard Worker
341*6dbdd20aSAndroid Build Coastguard Workerdef do_desc(options, graph):
342*6dbdd20aSAndroid Build Coastguard Worker  print('Rules:')
343*6dbdd20aSAndroid Build Coastguard Worker  for rule in flatten_rules(DEPS_ALLOWLIST):
344*6dbdd20aSAndroid Build Coastguard Worker    print(' - %s' % rule)
345*6dbdd20aSAndroid Build Coastguard Worker
346*6dbdd20aSAndroid Build Coastguard Worker
347*6dbdd20aSAndroid Build Coastguard Workerdef do_print(options, graph):
348*6dbdd20aSAndroid Build Coastguard Worker  for node, edges in graph.items():
349*6dbdd20aSAndroid Build Coastguard Worker    for edge in edges:
350*6dbdd20aSAndroid Build Coastguard Worker      print("{}\t{}".format(node, edge))
351*6dbdd20aSAndroid Build Coastguard Worker
352*6dbdd20aSAndroid Build Coastguard Worker
353*6dbdd20aSAndroid Build Coastguard Workerdef do_dot(options, graph):
354*6dbdd20aSAndroid Build Coastguard Worker
355*6dbdd20aSAndroid Build Coastguard Worker  def simplify(path):
356*6dbdd20aSAndroid Build Coastguard Worker    if is_external_dep(path):
357*6dbdd20aSAndroid Build Coastguard Worker      return path
358*6dbdd20aSAndroid Build Coastguard Worker    return os.path.dirname(path)
359*6dbdd20aSAndroid Build Coastguard Worker
360*6dbdd20aSAndroid Build Coastguard Worker  new_graph = collections.defaultdict(set)
361*6dbdd20aSAndroid Build Coastguard Worker  for node, edges in graph.items():
362*6dbdd20aSAndroid Build Coastguard Worker    for edge in edges:
363*6dbdd20aSAndroid Build Coastguard Worker      new_graph[simplify(edge)]
364*6dbdd20aSAndroid Build Coastguard Worker      new_graph[simplify(node)].add(simplify(edge))
365*6dbdd20aSAndroid Build Coastguard Worker  graph = new_graph
366*6dbdd20aSAndroid Build Coastguard Worker
367*6dbdd20aSAndroid Build Coastguard Worker  if options.ignore_external:
368*6dbdd20aSAndroid Build Coastguard Worker    new_graph = collections.defaultdict(set)
369*6dbdd20aSAndroid Build Coastguard Worker    for node, edges in graph.items():
370*6dbdd20aSAndroid Build Coastguard Worker      if is_external_dep(node):
371*6dbdd20aSAndroid Build Coastguard Worker        continue
372*6dbdd20aSAndroid Build Coastguard Worker      for edge in edges:
373*6dbdd20aSAndroid Build Coastguard Worker        if is_external_dep(edge):
374*6dbdd20aSAndroid Build Coastguard Worker          continue
375*6dbdd20aSAndroid Build Coastguard Worker        new_graph[edge]
376*6dbdd20aSAndroid Build Coastguard Worker        new_graph[node].add(edge)
377*6dbdd20aSAndroid Build Coastguard Worker    graph = new_graph
378*6dbdd20aSAndroid Build Coastguard Worker
379*6dbdd20aSAndroid Build Coastguard Worker  write_dot(graph, sys.stdout)
380*6dbdd20aSAndroid Build Coastguard Worker  return 0
381*6dbdd20aSAndroid Build Coastguard Worker
382*6dbdd20aSAndroid Build Coastguard Worker
383*6dbdd20aSAndroid Build Coastguard Workerdef main():
384*6dbdd20aSAndroid Build Coastguard Worker  parser = argparse.ArgumentParser(description=__doc__)
385*6dbdd20aSAndroid Build Coastguard Worker  parser.set_defaults(func=do_check)
386*6dbdd20aSAndroid Build Coastguard Worker  subparsers = parser.add_subparsers()
387*6dbdd20aSAndroid Build Coastguard Worker
388*6dbdd20aSAndroid Build Coastguard Worker  check_command = subparsers.add_parser(
389*6dbdd20aSAndroid Build Coastguard Worker      'check', help='Check the rules (default)')
390*6dbdd20aSAndroid Build Coastguard Worker  check_command.set_defaults(func=do_check)
391*6dbdd20aSAndroid Build Coastguard Worker
392*6dbdd20aSAndroid Build Coastguard Worker  desc_command = subparsers.add_parser('desc', help='Print the rules')
393*6dbdd20aSAndroid Build Coastguard Worker  desc_command.set_defaults(func=do_desc)
394*6dbdd20aSAndroid Build Coastguard Worker
395*6dbdd20aSAndroid Build Coastguard Worker  print_command = subparsers.add_parser('print', help='Print all imports')
396*6dbdd20aSAndroid Build Coastguard Worker  print_command.set_defaults(func=do_print)
397*6dbdd20aSAndroid Build Coastguard Worker
398*6dbdd20aSAndroid Build Coastguard Worker  dot_command = subparsers.add_parser(
399*6dbdd20aSAndroid Build Coastguard Worker      'dot',
400*6dbdd20aSAndroid Build Coastguard Worker      help='Output dependency graph in dot format suitble for use in ' +
401*6dbdd20aSAndroid Build Coastguard Worker      'graphviz (e.g. ./tools/check_imports dot | dot -Tpng -ograph.png)')
402*6dbdd20aSAndroid Build Coastguard Worker  dot_command.set_defaults(func=do_dot)
403*6dbdd20aSAndroid Build Coastguard Worker  dot_command.add_argument(
404*6dbdd20aSAndroid Build Coastguard Worker      '--ignore-external',
405*6dbdd20aSAndroid Build Coastguard Worker      action='store_true',
406*6dbdd20aSAndroid Build Coastguard Worker      help='Don\'t show external dependencies',
407*6dbdd20aSAndroid Build Coastguard Worker  )
408*6dbdd20aSAndroid Build Coastguard Worker
409*6dbdd20aSAndroid Build Coastguard Worker  # This is a general import graph of the form /plugins/foo/index -> /base/hash
410*6dbdd20aSAndroid Build Coastguard Worker  graph = collections.defaultdict(set)
411*6dbdd20aSAndroid Build Coastguard Worker
412*6dbdd20aSAndroid Build Coastguard Worker  # Build the dep graph
413*6dbdd20aSAndroid Build Coastguard Worker  for path in all_source_files():
414*6dbdd20aSAndroid Build Coastguard Worker    for src, target, _ in find_imports(path):
415*6dbdd20aSAndroid Build Coastguard Worker      graph[src].add(target)
416*6dbdd20aSAndroid Build Coastguard Worker      graph[target]
417*6dbdd20aSAndroid Build Coastguard Worker
418*6dbdd20aSAndroid Build Coastguard Worker  options = parser.parse_args()
419*6dbdd20aSAndroid Build Coastguard Worker  return options.func(options, graph)
420*6dbdd20aSAndroid Build Coastguard Worker
421*6dbdd20aSAndroid Build Coastguard Worker
422*6dbdd20aSAndroid Build Coastguard Workerif __name__ == '__main__':
423*6dbdd20aSAndroid Build Coastguard Worker  sys.exit(main())
424