Rewrite iwyu.py and format-code.py with various improvements

Signed-off-by: Michael Richters <gedankenexperimenter@gmail.com>
pull/1162/head
Michael Richters 2 years ago
parent a7e6f3f072
commit fa0bb377c7
No known key found for this signature in database
GPG Key ID: 1288FD13E4EEF0C0

@ -0,0 +1,110 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
# Copyright (c) 2022 Michael Richters <gedankenexperimenter@gmail.com>
# This is free and unencumbered software released into the public domain.
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
# For more information, please refer to <http://unlicense.org/>
# ------------------------------------------------------------------------------
"""Utilities shared by Kaleidoscope code maintenance tools."""
import logging
import os
import subprocess
import sys
from contextlib import contextmanager
# ==============================================================================
@contextmanager
def cwd(temp_wd):
"""Execute code in a different working directory
Parameters:
`temp_wd` (`str`): The name of a directory to (temporarily) change to
Change the current working directory, then automatically restore the previous working
directory when done. Invoke `cwd()` like this:
```py
with cwd(temp_wd):
...
```
"""
old_wd = os.getcwd()
os.chdir(temp_wd)
try:
yield
finally:
os.chdir(old_wd)
# ==============================================================================
def split_on_newlines(string):
"""Split the string using newlines as the separator."""
return string.splitlines()
# ------------------------------------------------------------------------------
def split_on_nulls(string):
"""Split the input string using NULL characters as the separator."""
targets = [_ for _ in string.split('\0') if _ != '']
return targets or []
# ==============================================================================
def setup_logging(loglevel):
"""Set up basic logging."""
logformat = "%(message)s"
logging.basicConfig(
level=loglevel,
stream=sys.stdout,
format=logformat,
datefmt="",
)
return logging.getLogger()
# ==============================================================================
def check_git_diff():
"""Check for unstaged changes with `git diff`
Returns: a list of the names of files with unstaged changes
This check isn't perfect, because it can give false positives (if there are unrelated
unstaged changes).
"""
git_diff_cmd = ['git', 'diff', '-z', '--exit-code', '--name-only']
changed_files = []
proc = subprocess.run(git_diff_cmd, capture_output=True)
if proc.returncode != 0:
changed_files = split_on_nulls(proc.stdout.decode('utf-8'))
return changed_files
# ==============================================================================
if __name__ == "__main__":
sys.exit(1)

@ -28,23 +28,27 @@
# For more information, please refer to <http://unlicense.org/>
# ------------------------------------------------------------------------------
"""This script runs clang-format on Kaleidoscope's codebase."""
"""This script runs clang-format on a Kaleidoscope repository."""
import argparse
import glob
import logging
import os
import re
import shutil
import subprocess
import sys
sys.dont_write_bytecode = True
from common import check_git_diff, setup_logging, split_on_newlines, split_on_nulls
# ==============================================================================
def parse_args(args):
"""Parse command line parameters
Args:
args (List[str]): command line parameters as list of strings
args (list[str]): command line parameters as list of strings
(for example ``["--help"]``).
Returns:
@ -55,217 +59,215 @@ def parse_args(args):
Recursively search specified directories and format source files with
clang-format. By default, it operates on Arduino C++ source files with
extensions: *.{cpp,h,hpp,inc,ino}.""")
parser.add_argument(
'-q',
'--quiet',
dest='loglevel',
action='store_const',
const=logging.ERROR,
default=logging.WARNING,
help="""
Suppress output except warnings and errors.""",
)
parser.add_argument(
'-v',
'--verbose',
dest='loglevel',
help="Verbose output",
action='store_const',
dest='loglevel',
const=logging.INFO,
help="""
Output verbose debugging information.""",
)
parser.add_argument(
'-q',
'--quiet',
dest='loglevel',
help="Suppress all non-error output",
'-d',
'--debug',
action='store_const',
const=logging.ERROR,
dest='loglevel',
const=logging.DEBUG,
help="""
Save output from `include-what-you-use` for processed files beside the originals, with
a '.iwyu' suffix, for debugging purposes.""",
)
parser.add_argument(
'-X',
'--exclude-dir',
metavar="<path>",
dest='exclude_dirs',
help="Exclude dir from search (path relative to the pwd)",
action='append',
dest='exclude_dirs',
default=[],
metavar="<path>",
help="""
Exclude dir from search (path relative to the pwd)""",
)
parser.add_argument(
'-x',
'--exclude-file',
metavar="<file>",
dest='exclude_files',
help="Exclude <file> (base name only, not a full path) from formatting",
action='append',
dest='exclude_files',
default=[],
metavar="<file>",
help="""
Exclude <file> (base name only, not a full path) from formatting""",
)
parser.add_argument(
'-e',
'-r',
'--regex',
metavar="<regex>",
dest='src_re_str',
help="Regular expression for matching source file names",
dest='regex',
default=r'\.(cpp|h|hpp|inc|ino)$',
metavar="<regex>",
help="""
Regular expression for matching source file names""",
)
parser.add_argument(
'-z',
'-0',
action='store_const',
dest='input_splitter',
const=split_on_nulls,
default=split_on_newlines,
help="""
When reading target filenames from standard input, break on NULL characters instead
of newlines.""",
)
parser.add_argument(
'-f',
'--force',
action='store_true',
help="Format code even if there are unstaged changes",
help="""
Format code even if there are unstaged changes""",
)
parser.add_argument(
'--check',
action='store_true',
help="Check for changes after formatting",
help="""
Check for changes after formatting by running `git diff --exit-code`. If there are any
changes after formatting, a non-zero exit code is returned.""",
)
parser.add_argument(
'targets',
metavar="<search_dir>",
nargs='+',
help="""A list of files and/or directories to search for source files to format""",
nargs='*',
help="""
A list of files and/or directories to search for source files to format.""",
)
return parser.parse_args(args)
# ==============================================================================
def setup_logging(loglevel):
"""Setup basic logging
Args:
loglevel (int): minimum loglevel for emitting messages
def main():
"""Parse command-line arguments and format source files.
"""
logformat = "%(message)s"
logging.basicConfig(
level=loglevel,
stream=sys.stdout,
format=logformat,
datefmt="",
)
return logging.getLogger()
# ==============================================================================
def format_code(path, opts, clang_format_cmd):
"""Run clang-format on a directory."""
logging.info("Formatting code in %s...", path)
src_regex = re.compile(opts.src_re_str)
src_files = []
for root, dirs, files in os.walk(path):
for exclude_path in opts.exclude_dirs:
exclude_path = exclude_path.rstrip(os.path.sep)
if os.path.dirname(exclude_path) == root:
exclude_dir = os.path.basename(exclude_path)
if exclude_dir in dirs:
dirs.remove(exclude_dir)
for name in files:
if name in opts.exclude_files:
continue
if src_regex.search(name):
src_files.append(os.path.join(root, name))
proc = subprocess.run(clang_format_cmd + src_files)
# Parse command-line argumets:
opts = parse_args(sys.argv[1:])
# Set up logging system:
setup_logging(opts.loglevel)
# ----------------------------------------------------------------------
# Unless we've been given the `--force` option, check for unstaged changes to avoid
# clobbering any work in progress:
exit_code = 0
if not opts.force:
changed_files = check_git_diff()
if len(changed_files) > 0:
logging.error("Working tree has unstaged changes; aborting")
return 1
# Locate `clang-format` executable:
clang_format_exe = shutil.which('clang-format')
logging.debug("Found `clang-format` executable: %s", clang_format_exe)
clang_format_cmd = [clang_format_exe, '-i']
if opts.loglevel <= logging.INFO:
clang_format_cmd.append('--verbose')
# ----------------------------------------------------------------------
# Read targets from command line:
targets = opts.targets
logging.debug("CLI target parameters: %s", targets)
# If stdin is a pipe, read target filenames from it:
if not sys.stdin.isatty():
targets += opts.input_splitter(sys.stdin.read())
logging.debug("All targets: %s", targets)
# Prepare exclusion lists. The file excludes are basenames only, and the dirs get
# converted to absolute path names.
exclude_files = set(opts.exclude_files)
exclude_dirs = set(os.path.abspath(_) for _ in opts.exclude_dirs)
# Convert target paths to absolute, and remove any that are excluded:
target_paths = set(os.path.abspath(_) for _ in targets if _ not in exclude_dirs)
logging.debug("Target paths: %s", target_paths)
# Build separate sets of target files and dirs. Later, we'll search target dirs and add
# matching target files to the files set.
target_files = set()
target_dirs = set()
for t in target_paths:
if os.path.isfile(t):
target_files.add(os.path.abspath(t))
elif os.path.isdir(t):
target_dirs.add(os.path.abspath(t))
logging.debug("Target files after separating: %s", target_files)
logging.debug("Target dirs after separating: %s", target_dirs)
# Remove excluded filenames:
target_files -= set(_ for _ in target_files if os.path.basename(_) in exclude_files)
# Remove files and dirs in excluded dirs:
target_files -= set(_ for _ in target_files for x in exclude_dirs if _.startswith(x))
target_dirs -= set(_ for _ in target_dirs for x in exclude_dirs if _.startswith(x))
# Compile regex for matching files to be formatted:
target_matcher = re.compile(opts.regex)
# Remove target files that don't match the regex:
logging.debug("Target files before matching regex: %s", target_files)
target_files = set(_ for _ in target_files if target_matcher.search(_))
logging.debug("Target files after matching regex: %s", target_files)
# Search target dirs for non-excluded files, and add them to `target_files`:
logging.debug("Searching target dirs: %s", target_dirs)
for path in target_dirs:
for root, dirs, files in os.walk(path):
# Prune excluded dirs
for x in exclude_dirs:
if x in (os.path.join(root, _) for _ in dirs):
dirs.remove(os.path.basename(x))
# Add non-excluded files
for f in files:
if target_matcher.search(f) and f not in exclude_files:
target_files.add(os.path.join(root, f))
if len(target_files) == 0:
logging.error("No target files found; exiting.")
return 1
# Run clang-format on target files:
proc = subprocess.run(clang_format_cmd + sorted(target_files))
if proc.returncode != 0:
logging.error("Error: clang-format returned non-zero status: %s", proc.returncode)
return
# ==============================================================================
def build_file_list(path, src_regex):
"""Docstring"""
# If the specified path is a filename, return it (as a list), regardless of
# whether or not it matches the regex.
if os.path.isfile(path):
return [path]
# If the specified path is not valid, just return an empty list.
if not os.path.isdir(path):
return []
# The specified path is a directory, so we search recursively for files
# contained therein that match the specified regular expression.
source_files = []
for root, dirs, files in os.walk(path):
# First, ignore all dotfiles (and directories).
dotfiles = set(glob.glob('.*'))
dirs = set(dirs) - dotfiles
files = set(dirs) - dotfiles
# Check for a list of file glob patterns that should be excluded.
if IWYU_IGNORE_FILE in files:
with open(os.path.join(root, IWYU_IGNORE_FILE)) as f:
for pattern in f.read().splitlines():
matches = set(glob.glob(os.path.join(root, pattern)))
dirs = set(dirs) - matches
files = set(files) - matches
# Add all matching files to the list of source files to be formatted.
for f in filter(src_regex.search, files):
source_files.append(os.path.join(root, f))
return source_files
return proc.returncode
else:
logging.info("Finished formatting target files.")
# ==============================================================================
def warn_if_output(byte_str, msg):
"""Convert a string of bytes to a UTF-8 string, break it on newlines. If
there is any output, print a warning message, followed by each line,
indented by four spaces."""
lines = byte_str.decode('utf-8').splitlines()
if '' in lines:
lines.remove('')
if len(lines) > 0:
logging.warning('%s', msg)
for line in lines:
logging.warning(' %s', line)
return
# ==============================================================================
def main(cli_args):
"""Parse command-line arguments and format source files."""
args = parse_args(cli_args)
if args.loglevel is None:
args.loglevel = logging.WARNING
setup_logging(args.loglevel)
clang_format = os.getenv('CLANG_FORMAT_CMD')
if clang_format is None:
clang_format = 'clang-format'
clang_format_cmd = [clang_format, '-i']
git_diff_cmd = ['git', 'diff', '--exit-code']
proc = subprocess.run(git_diff_cmd + ['--name-only'], capture_output=True)
if proc.returncode != 0:
warn_if_output(proc.stdout, 'Warning: you have unstaged changes to these files:')
if not args.force:
logging.warning(
'Formatting aborted. Stage your changes or use --force to override.')
sys.exit(proc.returncode)
if args.loglevel >= logging.WARNING:
git_diff_cmd.append('--quiet')
elif args.loglevel <= logging.INFO:
git_diff_cmd.append('--name-only')
for path in args.targets:
format_code(path, args, clang_format_cmd)
if args.check:
# If we've been asked to check for changes made by the formatter:
if opts.check:
logging.warning('Checking for changes made by the formatter...')
changed_files = check_git_diff()
if len(changed_files) == 0:
logging.warning("No files changed. Congratulations!")
else:
logging.warning("Found files with changes after formatting:")
exit_code = 1
for f in changed_files:
logging.warning(" %s", f)
proc = subprocess.run(git_diff_cmd + ['--cached'], capture_output=True)
if proc.returncode != 0:
logging.warning(
'Warning: Your working tree has staged changes. ' +
'Committed changes might not pass this check.')
warn_if_output(proc.stdout, 'The following files have unstaged changes:')
proc = subprocess.run(git_diff_cmd, capture_output=True)
if proc.returncode != 0:
warn_if_output(proc.stdout, 'The following files have changes:')
logging.error(
'Check failed: Please commit formatting changes before submitting.')
sys.exit(proc.returncode)
return
return exit_code
# ==============================================================================
if __name__ == "__main__":
main(sys.argv[1:])
try:
sys.exit(main())
except KeyboardInterrupt:
logging.info("Aborting")
sys.exit(1)

@ -28,7 +28,9 @@
# For more information, please refer to <http://unlicense.org/>
# ------------------------------------------------------------------------------
"""This is a script for maintenance of the headers included in Kaleidoscope source
"""Run `include-what-you-use` on Kaleidoscope sources
This is a script for maintenance of the headers included in Kaleidoscope source
files. It is not currently possible to run this automatically on all
Kaleidoscope source files, because of the peculiarities therein. It uses
llvm/clang to determine which headers should be included in a given file, but
@ -55,12 +57,25 @@ import shutil
import subprocess
import sys
sys.dont_write_bytecode = True
from common import cwd, setup_logging, split_on_newlines, split_on_nulls
# ==============================================================================
def parse_args(args):
"""Parse command line parameters
Args:
args (list[str]): command line parameters as list of strings
(for example ``["--help"]``).
Returns:
:obj:`argparse.Namespace`: command line parameters namespace
"""
parser = argparse.ArgumentParser(
description=
"""Run `include-what-you-use` on source files given as command-line arguments and/or read
description="""
Run `include-what-you-use` on source files given as command-line arguments and/or read
from standard input. When reading target filenames from standard input, they should be
either absolute or relative to the current directory, and each line of input (minus the
line-ending character(s) is treated as a filename.""")
@ -68,65 +83,87 @@ def parse_args(args):
'-q',
'--quiet',
dest='loglevel',
help="Suppress output except warnings and errors.",
action='store_const',
const=logging.ERROR,
default=logging.WARNING,
help="""
Suppress output except warnings and errors.""",
)
parser.add_argument(
'-v',
'--verbose',
dest='loglevel',
help="Output verbose debugging information.",
action='store_const',
dest='loglevel',
const=logging.INFO,
help="""
Output verbose debugging information.""",
)
parser.add_argument(
'-d',
'--debug',
dest='loglevel',
help="""Save output from `include-what-you-use` for processed files beside the
originals, with a '.iwyu' suffix, for debugging purposes.""",
action='store_const',
dest='loglevel',
const=logging.DEBUG,
help="""
Save output from `include-what-you-use` for processed files beside the originals, with
a '.iwyu' suffix, for debugging purposes.""",
)
parser.add_argument(
'-r',
'--regex',
action='store',
dest='regex',
help="""A regular expression for matching filenames Only the basename of the file is
default=r'\.(h|cpp)$',
help="""
A regular expression for matching filenames Only the basename of the file is
matched, and the regex is only used when searching a directory for files to process,
not on target filenames specified in arguments or read from standard input.""",
action='store',
default=r'\.(h|cpp)$',
)
parser.add_argument(
'-i',
'--ignores_file',
dest='ignores_file',
metavar='<ignores_file>',
help=
"""The name of a file (relative to KALEIDOSCOPE_DIR) that contains a list of glob
patterns that will be ignored when a target directory is searched for filenames that
match <regex>.""",
action='store',
dest='ignores_file',
default='.iwyu_ignore',
metavar='<ignores_file>',
help="""
The name of a file (relative to KALEIDOSCOPE_DIR) that contains a list of glob patterns
that will be ignored when a target directory is searched for filenames that match
<regex>.""",
)
parser.add_argument(
'-I',
'--include',
action='append',
dest='include_dirs',
metavar='<dir>',
help=
"""Add <dir> to the list of directories that will be searched for header files.""",
action='append',
help="""
Add <dir> to the list of directories that will be searched for header files.""",
)
parser.add_argument(
'-z',
'-0',
action='store_const',
dest='input_splitter',
const=split_on_nulls,
default=split_on_newlines,
help="""
When reading target filenames from standard input, break on NULL characters instead
of newlines.""",
)
parser.add_argument(
'--update_comments',
action='store_true',
help="""
Call `include-what-you-use` with `--update_comments` to always rewrite its 'for'
comments regarding which symbols are used.""",
)
parser.add_argument(
'targets',
nargs='*',
metavar="<target>",
nargs='+',
help=
"""A list of target files and/or directories to search for source files to format. Any
help="""
A list of target files and/or directories to search for source files to format. Any
target file will be processed, regardless of the filename. Any target directory will
be recursively searched for files matching the regular expression given by --regex.
Filenames and directories beginning with a '.' will always be excluded from the search,
@ -135,22 +172,6 @@ def parse_args(args):
return parser.parse_args(args)
# ==============================================================================
def setup_logging(loglevel):
"""Set up basic logging
Args:
:int:loglevel: minimum loglevel for emitting messages
"""
logformat = "%(message)s"
logging.basicConfig(
level=loglevel,
stream=sys.stdout,
format=logformat,
datefmt="",
)
# ==============================================================================
def main():
"""Main entry point function."""
@ -162,12 +183,13 @@ def main():
# ----------------------------------------------------------------------
# Find include-what-you-use:
iwyu = shutil.which('include-what-you-use')
logging.info("Found `include-what-you-use` executable: %s", iwyu)
logging.debug("Found `include-what-you-use` executable: %s", iwyu)
iwyu_opts = [
'--no_fwd_decls', # No forward declarations
'--max_line_length=100',
'--update_comments',
]
if opts.update_comments:
iwyu_opts.append('--update_comments')
# Prepend '-Xiwyu' to each `include-what-you-use` option:
iwyu_opts = [_ for opt in iwyu_opts for _ in ('-Xiwyu', opt)]
@ -265,8 +287,9 @@ def main():
]
# Include plugin source dirs for plugins that depend on other plugins:
includes += glob.glob(os.path.join(kaleidoscope_dir, 'plugins', '*', 'src'))
# Include dirs specified on the command line:
includes += map(os.path.abspath, opts.include_dirs)
# Include dirs specified on the command line, if any:
if opts.include_dirs:
includes += [os.path.abspath(_) for _ in opts.include_dirs]
clang_opts += ['-I' + _ for _ in includes]
# ----------------------------------------------------------------------
@ -276,19 +299,23 @@ def main():
fix_includes_cmd = [
fix_includes,
'--update_comments',
'--nosafe_headers',
'--reorder',
'--separate_project_includes=' + kaleidoscope_src_dir, # Does this help?
]
if opts.update_comments:
fix_includes_cmd.append('--update_comments')
logging.debug("Using `fix_includes` command: %s", ' \\\n\t'.join(fix_includes_cmd))
# ----------------------------------------------------------------------
targets = opts.targets
# If stdin is a pipe, read pathname targets, one per line. This allows us to
# connect the output of `find` to our input conveniently:
# If stdin is a pipe, read pathname targets, one per line. This allows us to connect the
# output of `find` to our input conveniently:
if not sys.stdin.isatty():
targets += sys.stdin.read().splitlines()
logging.debug("Reading targets from STDIN...")
targets += opts.input_splitter(sys.stdin.read())
for t in targets:
logging.debug(" Target path: %s", t)
# ----------------------------------------------------------------------
iwyu_ignores_file = os.path.join(kaleidoscope_dir, opts.ignores_file)
@ -300,101 +327,138 @@ def main():
# relying on a header included by its associated header, but which that header does not
# need on its own. In this case, if we process the header first, IWYU won't be able to
# parse the source file, and we'll get an error, but if we do them in the other order,
# it'll be fine.
# the source file will get all the includes it needs before the header removes any of them.
source_files = []
header_files = []
for target_file in (_ for t in targets for _ in build_target_list(t, regex)):
for target_file in (_ for t in targets for _ in build_target_list(t, regex, ignores)):
if target_file.endswith('.cpp') or target_file.endswith('.ino'):
source_files.append(target_file)
else:
header_files.append(target_file)
# If there's an error processing any file, return an error code.
exit_code = 0
for target_file in source_files + header_files:
if target_file in ignores:
logging.info("Skipping ignored file: %s", os.path.relpath(target_file))
continue
# Run IWYU and fix_headers:
if not run_iwyu(os.path.relpath(target_file), iwyu_cmd, fix_includes_cmd):
exit_code = 1
return exit_code
# ==============================================================================
def build_target_list(path, src_regex):
"""Docstring"""
logging.debug("Searching target: %s", path)
def build_target_list(target, regex, ignores):
"""Build the list of target files, starting from a file or directory.
Parameters:
target (str): The name of the file or directory to search
regex (Pattern): A compiled regular expression to match target file names
ignores (list): A list of (absolute) file names to be excluded from the target list
Returns:
targets (list): A list of (absolute) file names to be processed
Given a filename or directory (`path`), returns a list of target filenames to run IWYU
on. Target filenames will all be absolute, but `path` can be relative to the current
directory. All target filenames will match `regex`, and any items in `ignores` will be
excluded. If an element of the `ignores` list is a directory, all files and directories
beneath it will be excluded from the search.
"""
logging.debug("Searching target: %s", target)
# If the specified path is a filename, return it (as a list), regardless of
# whether or not it matches the regex.
if os.path.isfile(path):
return [path]
# Convert all paths to absolute.
root = os.path.abspath(target)
# If `path` is a file, and it matches `regex`, add it to the target list.
if os.path.isfile(target) and regex.search(target) and root not in ignores:
return [root]
# If the specified path is not valid, just return an empty list.
if not os.path.isdir(path):
logging.error("Error: File not found: %s", path)
if not os.path.isdir(target):
logging.error("Error: File not found: %s", target)
return []
# Start with an empty list.
targets = []
# The specified path is a directory, so we search recursively for files
# contained therein that match the specified regular expression.
targets = []
for root, dirs, files in os.walk(os.path.abspath(path)):
logging.debug("Searching dir: %s", root)
for path, dirs, files in os.walk(root):
logging.debug("Searching dir: %s", os.path.relpath(path))
# First, ignore all dotfiles (and directories).
dotfiles = set(glob.glob('.*'))
dirs = set(dirs) - dotfiles
files = set(files) - dotfiles
for x in (os.path.basename(_)
for _ in ignores + glob.glob(os.path.join(path, '.*'))
if os.path.dirname(_) == path):
if x in dirs:
logging.info("Skipping ignored dir: %s", os.path.join(path, x))
dirs.remove(x)
if x in files:
logging.info("Skipping ignored file: %s", os.path.join(path, x))
files.remove(x)
logging.debug("Files found: %s", ', '.join(files))
# Add all matching files to the list of source files to be formatted.
for f in filter(src_regex.search, files):
logging.debug("Source file found: %s", f)
targets.append(os.path.join(root, f))
for f in (_ for _ in files if regex.search(_)):
t = os.path.join(path, f)
logging.debug("Source file found: %s", t)
targets.append(t)
return [os.path.abspath(_) for _ in targets]
return targets
# ==============================================================================
def build_ignores_list(ignores_file_path):
"""Build a list of files and dirs to exclude from processing by IWYU
Parameters:
ignores_file_path (str): The name of the file to read ignores globs from
Returns:
ignores_list (list): A list of (absolute) file names to exclude from processing
Reads `ignores_file` and expands each line as a glob, returning a list of all the target
file names to be excluded from processing. Each path in the file is treated as a relative
pathname (unless it is already absolute), relative to the directory in which `ignores_file`
is located.
"""
logging.debug("Searching for ignores file: %s", ignores_file_path)
# If the ignores file doesn't exist, return an empty list:
if not os.path.isfile(ignores_file_path):
logging.debug("Ignores file not found")
return []
ignores_list = []
with open(ignores_file_path) as f:
for line in f.read().splitlines():
logging.debug("Ignoring files like: %s", line)
# Lines starting with `#` are treated as comments.
if line.startswith('#'):
continue
logging.debug("Ignoring files like: %s", line)
ignores_list += glob.glob(line, recursive=True)
# Get the dir of the ignores file so we can construct absolute pathnames.
ignores_file_dir = os.path.dirname(ignores_file_path)
with cwd(ignores_file_dir):
ignores_list[:] = [os.path.abspath(_) for _ in ignores_list]
logging.debug("Ignores list:\n\t%s", "\n\t".join(ignores_list))
return ignores_list
# ------------------------------------------------------------------------------
from contextlib import contextmanager
@contextmanager
def cwd(path):
"""A simple function change directory, an automatically restore the previous working
directory when done, using `with cwd(temp_dir):`"""
old_wd = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old_wd)
# ==============================================================================
def run_iwyu(source_file, iwyu_cmd, fix_includes_cmd):
"""Run `include-what-you-use` on <source_file>, an update that file's header includes by
"""Run IWYU and fix_includes on a single source file
Parameters:
source_file (str): The name of a file to run IWYU on
iwyu_cmd (list): The command name and options list for `include-what-you-use`
fix_includes_cmd (list): The command name and options list for `fix_headers.py`
Returns:
True on success, False if either IWYU or fix_headers returns an error code
Run `include-what-you-use` on <source_file>, an update that file's header includes by
sending the output to `fix_includes.py`. If either command returns an error code, return
`False`, otherwise return `True`."""
logging.info("Processing file: %s", source_file)
`False`, otherwise return `True`.
"""
logging.info("Fixing headers in file: %s", source_file)
# Run IWYU on <source_file>
iwyu_proc = subprocess.run(iwyu_cmd + [source_file], capture_output=True, check=False)

Loading…
Cancel
Save