chromium/tools/style_variable_generator/find_invalid_css_variables.py

# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import argparse
import sys
import os
import re

sys.path += [os.path.dirname(os.path.dirname(__file__))]

from style_variable_generator.css_generator import CSSStyleGenerator
from style_variable_generator.presubmit_support import RunGit


# TODO(calamity): extend this checker to find unused C++ variables
def FindInvalidCSSVariables(file_to_json_strings, git_runner=RunGit):
    style_generator = CSSStyleGenerator()
    css_prefixes = set()
    referenced_vars = set()
    for f, json_string in file_to_json_strings.items():
        style_generator.AddJSONToModel(json_string, in_file=f)
        referenced_vars |= set(re.findall(r'\$([a-z_0-9]+)', json_string))

        context = style_generator.in_file_to_context.get(f, {}).get('CSS')
        if (not context or 'prefix' not in context):
            raise KeyError('This tool only works on files with a CSS prefix.')

        css_prefixes.add('--' + context['prefix'] + '-')

    unspecified_file_and_names = []
    css_var_names = style_generator.GetCSSVarNames()
    valid_names = set(css_var_names.keys())
    unused = set(css_var_names.values()).difference(referenced_vars)

    for css_prefix in css_prefixes:
        grep_result = git_runner([
            'grep', '-on',
            '\\%s[a-z0-9-]*' % css_prefix, '--', '*.css', '*.html', '*.js'
        ]).decode('utf-8').splitlines()
        found_files_and_names = [x.split(':') for x in grep_result]
        found_names = set()
        for (filename, line, name) in found_files_and_names:
            found_names.add(name)
            if name in valid_names and css_var_names[name] in unused:
                unused.remove(css_var_names[name])

        unspecified = found_names.difference(valid_names)
        for (filename, line, name) in found_files_and_names:
            if filename.find('test') != -1:
                continue

            if name in unspecified:
                unspecified_file_and_names.append('%s:%s:%s' %
                                                  (filename, line, name))

    return {
        'unspecified': unspecified_file_and_names,
        # TODO(calamity): This should also account for names referenced in json5
        # files.
        'unused': unused,
        'css_prefix': css_prefix,
    }


def main():
    parser = argparse.ArgumentParser(
        description='''Finds CSS variables in the codebase that are prefixed
        with |input_files|' CSS prefix but aren't specified in |input_files|.'''
    )
    parser.add_argument('targets', nargs='+', help='source json5 color files', )
    args = parser.parse_args()

    input_files = args.targets

    file_to_json_strings = {}
    for input_file in input_files:
        with open(input_file, 'r') as f:
            file_to_json_strings[input_file] = f.read()

    result = FindInvalidCSSVariables(file_to_json_strings)

    print('Has prefix %s but not in %s:' % (result['css_prefix'], input_files))
    for name in sorted(result['unspecified']):
        print(name)

    print('\nGenerated by %s but not used in codebase:' % input_files)
    for name in sorted(result['unused']):
        print(name)

    return 0


if __name__ == '__main__':
    sys.exit(main())