xref: /aosp_15_r20/external/perfetto/python/tools/check_ratchet.py (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1#!/usr/bin/env python3
2# Copyright (C) 2023 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""
16Force the reduction in use of some methods/types over time.
17Often a method ('LEGACY_registerTrackController') or a type ('any')
18gets replaced by a better alternative ('registerTrack', 'unknown') and
19we want to a. replace all existing uses, b. prevent the introduction of
20new uses. This presubmit helps with both. It keeps a count of the
21number of instances of "FOO" in the codebase. At presubmit time we run
22the script. If the "FOO" count has gone up we encourage the author to
23use the alternative. If the "FOO" count has gone down we congratulate
24them and prompt them to reduce the expected count.
25Since the number of "FOO"s can only go down eventually they will all
26be gone - completing the migration.
27See also https://qntm.org/ratchet.
28"""
29
30import sys
31import os
32import re
33import argparse
34import collections
35import dataclasses
36
37from dataclasses import dataclass
38
39EXPECTED_ANY_COUNT = 50
40EXPECTED_RUN_METRIC_COUNT = 4
41
42ROOT_DIR = os.path.dirname(
43    os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
44UI_SRC_DIR = os.path.join(ROOT_DIR, 'ui', 'src')
45
46
47@dataclasses.dataclass
48class Check:
49  regex: str
50  expected_count: int
51  expected_variable_name: str
52  description: str
53
54
55CHECKS = [
56    # 'any' is too generic. It will show up in many comments etc. So
57    # instead of counting any directly we forbid it using eslint and count
58    # the number of suppressions.
59    Check(r"// eslint-disable-next-line @typescript-eslint/no-explicit-any",
60          EXPECTED_ANY_COUNT, "EXPECTED_ANY_COUNT",
61          "We should avoid using any whenever possible. Prefer unknown."),
62    Check(
63        r"RUN_METRIC\(", EXPECTED_RUN_METRIC_COUNT, "EXPECTED_RUN_METRIC_COUNT",
64        "RUN_METRIC() is not a stable trace_processor API. Use a stdlib function or macro. See https://perfetto.dev/docs/analysis/perfetto-sql-syntax#defining-functions."
65    ),
66]
67
68
69def all_source_files():
70  for root, dirs, files in os.walk(UI_SRC_DIR, followlinks=False):
71    for name in files:
72      if name.endswith('.ts'):
73        yield os.path.join(root, name)
74
75
76def do_check(options):
77  c = collections.Counter()
78
79  for path in all_source_files():
80    with open(path) as f:
81      s = f.read()
82    for check in CHECKS:
83      count = len(re.findall(check.regex, s))
84      c[check.expected_variable_name] += count
85
86  for check in CHECKS:
87    actual_count = c[check.expected_variable_name]
88
89    if actual_count > check.expected_count:
90      print(f'More "{check.regex}" {check.expected_count} -> {actual_count}')
91      print(
92          f'  Expected to find {check.expected_count} instances of "{check.regex}" accross the .ts & .d.ts files in the code base.'
93      )
94      print(f'  Instead found {actual_count}.')
95      print(
96          f'  It it likely your CL introduces additional uses of "{check.regex}".'
97      )
98      print(f'  {check.description}')
99      return 1
100    elif actual_count < check.expected_count:
101      print(f'Less "{check.regex}" {check.expected_count} -> {actual_count}')
102      print(
103          f'  Congratulations your CL reduces the instances of "{check.regex}" in the code base from {check.expected_count} to {actual_count}.'
104      )
105      print(
106          f'  Please go to {__file__} and set {check.expected_variable_name} to {actual_count}.'
107      )
108      return 1
109
110  return 0
111
112
113def main():
114  parser = argparse.ArgumentParser(description=__doc__)
115  parser.set_defaults(func=do_check)
116  subparsers = parser.add_subparsers()
117
118  check_command = subparsers.add_parser(
119      'check', help='Check the rules (default)')
120  check_command.set_defaults(func=do_check)
121
122  options = parser.parse_args()
123  return options.func(options)
124
125
126if __name__ == '__main__':
127  sys.exit(main())
128