367 lines
12 KiB
Python
Executable File
367 lines
12 KiB
Python
Executable File
#!/usr/bin/env 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()
|