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