#!/usr/bin/python2 # Copyright 2015 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Configuration Option Checker. Script to ensure that all configuration options for the Chrome EC are defined in config.h. """ from __future__ import print_function import os import re import subprocess class Line(object): """Class for each changed line in diff output. Attributes: line_num: The integer line number that this line appears in the file. string: The literal string of this line. line_type: '+' or '-' indicating if this line was an addition or deletion. """ def __init__(self, line_num, string, line_type): """Inits Line with the line number and the actual string.""" self.line_num = line_num self.string = string self.line_type = line_type class Hunk(object): """Class for a git diff hunk. Attributes: filename: The name of the file that this hunk belongs to. lines: A list of Line objects that are a part of this hunk. """ def __init__(self, filename, lines): """Inits Hunk with the filename and the list of lines of the hunk.""" self.filename = filename self.lines = lines # Master file which is supposed to include all CONFIG_xxxx descriptions. CONFIG_FILE = 'include/config.h' # Specific files which the checker should ignore. WHITELIST = [CONFIG_FILE, 'util/config_option_check.py'] def obtain_current_config_options(): """Obtains current config options from include/config.h. Scans through the master config file defined in CONFIG_FILE for all CONFIG_* options. Returns: config_options: A list of all the config options in the master CONFIG_FILE. """ config_options = [] config_option_re = re.compile(r'^#(define|undef)\s+(CONFIG_[A-Z0-9_]+)') with open(CONFIG_FILE, 'r') as config_file: for line in config_file: result = config_option_re.search(line) if not result: continue word = result.groups()[1] if word not in config_options: config_options.append(word) return config_options def obtain_config_options_in_use(): """Obtains all the config options in use in the repo. Scans through the entire repo looking for all CONFIG_* options actively used. Returns: options_in_use: A set of all the config options in use in the repo. """ file_list = [] cwd = os.getcwd() config_option_re = re.compile(r'\b(CONFIG_[a-zA-Z0-9_]+)') config_debug_option_re = re.compile(r'\b(CONFIG_DEBUG_[a-zA-Z0-9_]+)') options_in_use = set() for (dirpath, dirnames, filenames) in os.walk(cwd, topdown=True): # Ignore the build and private directories (taken from .gitignore) if 'build' in dirnames: dirnames.remove('build') if 'private' in dirnames: dirnames.remove('private') for f in filenames: # Ignore hidden files. if f.startswith('.'): continue # Only consider C source, assembler, and Make-style files. if (os.path.splitext(f)[1] in ('.c', '.h', '.inc', '.S', '.mk') or 'Makefile' in f): file_list.append(os.path.join(dirpath, f)) # Search through each file and build a set of the CONFIG_* options being # used. for f in file_list: if CONFIG_FILE in f: continue with open(f, 'r') as cur_file: for line in cur_file: match = config_option_re.findall(line) if match: for option in match: if not in_comment(f, line, option): if option not in options_in_use: options_in_use.add(option) # Since debug options can be turned on at any time, assume that they are # always in use in case any aren't being used. with open(CONFIG_FILE, 'r') as config_file: for line in config_file: match = config_debug_option_re.findall(line) if match: for option in match: if not in_comment(CONFIG_FILE, line, option): if option not in options_in_use: options_in_use.add(option) return options_in_use def print_missing_config_options(hunks, config_options): """Searches thru all the changes in hunks for missing options and prints them. Args: hunks: A list of Hunk objects which represent the hunks from the git diff output. config_options: A list of all the config options in the master CONFIG_FILE. Returns: missing_config_option: A boolean indicating if any CONFIG_* options are missing from the master CONFIG_FILE in this commit or if any CONFIG_* options removed are no longer being used in the repo. """ missing_config_option = False print_banner = True deprecated_options = set() # Determine longest CONFIG_* length to be used for formatting. max_option_length = max(len(option) for option in config_options) config_option_re = re.compile(r'\b(CONFIG_[a-zA-Z0-9_]+)') # Search for all CONFIG_* options in use in the repo. options_in_use = obtain_config_options_in_use() # Check each hunk's line for a missing config option. for h in hunks: for l in h.lines: # Check for the existence of a CONFIG_* in the line. match = config_option_re.findall(l.string) if not match: continue # At this point, an option was found in the line. However, we need to # verify that it is not within a comment. violations = set() for option in match: if not in_comment(h.filename, l.string, option): # Since the CONFIG_* option is not within a comment, we've found a # violation. We now need to determine if this line is a deletion or # not. For deletions, we will need to verify if this CONFIG_* option # is no longer being used in the entire repo. if l.line_type is '-': if option not in options_in_use and option in config_options: deprecated_options.add(option) else: violations.add(option) # Check to see if the CONFIG_* option is in the config file and print the # violations. for option in match: if option not in config_options and option in violations: # Print the banner once. if print_banner: print('The following config options were found to be missing ' 'from %s.\n' 'Please add new config options there along with ' 'descriptions.\n\n' % CONFIG_FILE) print_banner = False missing_config_option = True # Print the misssing config option. print('> %-*s %s:%s' % (max_option_length, option, h.filename, l.line_num)) if deprecated_options: print('\n\nThe following config options are being removed and also appear' ' to be the last uses\nof that option. Please remove these ' 'options from %s.\n\n' % CONFIG_FILE) for option in deprecated_options: print('> %s' % option) missing_config_option = True return missing_config_option def in_comment(filename, line, substr): """Checks if given substring appears in a comment. Args: filename: The filename where this line is from. This is used to determine what kind of comments to look for. line: String of line to search in. substr: Substring to search for in the line. Returns: is_in_comment: Boolean indicating if substr was in a comment. """ c_style_ext = ('.c', '.h', '.inc', '.S') make_style_ext = ('.mk') is_in_comment = False extension = os.path.splitext(filename)[1] substr_idx = line.find(substr) # Different files have different comment syntax; Handle appropriately. if extension in c_style_ext: beg_comment_idx = line.find('/*') end_comment_idx = line.find('*/') if end_comment_idx == -1: end_comment_idx = len(line) if beg_comment_idx == -1: # Check to see if this line is from a multi-line comment. if line.lstrip().startswith('* '): # It _seems_ like it is. is_in_comment = True else: # Check to see if its actually inside the comment. if beg_comment_idx < substr_idx < end_comment_idx: is_in_comment = True elif extension in make_style_ext or 'Makefile' in filename: beg_comment_idx = line.find('#') # Ignore everything to the right of the hash. if beg_comment_idx < substr_idx and beg_comment_idx != -1: is_in_comment = True return is_in_comment def get_hunks(): """Gets the hunks of the most recent commit. States: new_file: Searching for a new file in the git diff. filename_search: Searching for the filename of this hunk. hunk: Searching for the beginning of a new hunk. lines: Counting line numbers and searching for changes. Returns: hunks: A list of Hunk objects which represent the hunks in the git diff output. """ diff = [] hunks = [] hunk_lines = [] line = '' filename = '' i = 0 line_num = 0 # Regex patterns new_file_re = re.compile(r'^diff --git') filename_re = re.compile(r'^[+]{3} (.*)') hunk_line_num_re = re.compile(r'^@@ -[0-9]+,[0-9]+ \+([0-9]+),[0-9]+ @@.*') line_re = re.compile(r'^([+| |-])(.*)') # Get the diff output. cmd = 'git diff --cached -GCONFIG_* --no-prefix --no-ext-diff HEAD~1' diff = subprocess.check_output(cmd.split()).split('\n') line = diff[0] current_state = 'new_file' while True: # Search for the beginning of a new file. if current_state is 'new_file': match = new_file_re.search(line) if match: current_state = 'filename_search' # Search the diff output for a file name. elif current_state is 'filename_search': # Search for a file name. match = filename_re.search(line) if match: filename = match.groups(1)[0] if filename in WHITELIST: # Skip the file if it's whitelisted. current_state = 'new_file' else: current_state = 'hunk' # Search for a hunk. Each hunk starts with a line describing the line # numbers in the file. elif current_state is 'hunk': hunk_lines = [] match = hunk_line_num_re.search(line) if match: # Extract the line number offset. line_num = int(match.groups(1)[0]) current_state = 'lines' # Start looking for changes. elif current_state is 'lines': # Check if state needs updating. new_hunk = hunk_line_num_re.search(line) new_file = new_file_re.search(line) if new_hunk: current_state = 'hunk' hunks.append(Hunk(filename, hunk_lines)) continue elif new_file: current_state = 'new_file' hunks.append(Hunk(filename, hunk_lines)) continue match = line_re.search(line) if match: line_type = match.groups(1)[0] # We only care about modifications. if line_type is not ' ': hunk_lines.append(Line(line_num, match.groups(2)[1], line_type)) # Deletions don't count towards the line numbers. if line_type is not '-': line_num += 1 # Advance to the next line try: i += 1 line = diff[i] except IndexError: # We've reached the end of the diff. Return what we have. if hunk_lines: hunks.append(Hunk(filename, hunk_lines)) return hunks def main(): """Searches through committed changes for missing config options. Checks through committed changes for CONFIG_* options. Then checks to make sure that all CONFIG_* options used are defined in include/config.h. Finally, reports any missing config options. """ # Obtain the hunks of the commit to search through. hunks = get_hunks() # Obtain config options from include/config.h. config_options = obtain_current_config_options() # Find any missing config options from the hunks and print them. missing_opts = print_missing_config_options(hunks, config_options) if missing_opts: print('\nIt may also be possible that you have a typo.') os.sys.exit(1) if __name__ == '__main__': main()