Files
buildcache/lua-examples/clang_tidy_wrapper.lua

446 lines
13 KiB
Lua

-- match(.*(clang-tidy).*)
-------------------------------------------------------------------------------
-- Copyright (c) 2021 Marcus Geelnard
--
-- This software is provided 'as-is', without any express or implied warranty.
-- In no event will the authors be held liable for any damages arising from the
-- use of this software.
--
-- Permission is granted to anyone to use this software for any purpose,
-- including commercial applications, and to alter it and redistribute it
-- freely, subject to the following restrictions:
--
-- 1. The origin of this software must not be misrepresented; you must not
-- claim that you wrote the original software. If you use this software in
-- a product, an acknowledgment in the product documentation would be
-- appreciated but is not required.
--
-- 2. Altered source versions must be plainly marked as such, and must not be
-- misrepresented as being the original software.
--
-- 3. This notice may not be removed or altered from any source distribution.
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- This wrapper caches results for clang-tidy.
-------------------------------------------------------------------------------
-- For luacheck...
-- The following globals are provided by the BuildCache run-time environment:
-- luacheck: globals require_std ARGS bcache
require_std("*")
-------------------------------------------------------------------------------
-- Internal helper functions.
-------------------------------------------------------------------------------
local _OPTIONS_WITH_ARGS = {
["-p"] = true,
["--checks"] = true,
["--config"] = true,
["--export-fixes"] = true,
["--extra-arg"] = true,
["--extra-arg-before"] = true,
["--format-style"] = true,
["--header-filter"] = true,
["--line-filter"] = true,
["--store-check-profile"] = true,
["--vfsoverlay"] = true,
["--warnings-as-errors"] = true,
}
local function parse_arg (idx)
local arg = ARGS[idx]
local arg2
if _OPTIONS_WITH_ARGS[arg] then
arg2 = ARGS[idx+1]
return idx+2, arg, arg2
end
arg2 = arg:match(".*=(.*)")
if arg2 then
arg = arg:match("(.*)=.*")
end
return idx+1, arg, arg2
end
local function for_each_arg (f)
local i = 2
while i <= #ARGS do
local arg, arg2
i, arg, arg2 = parse_arg(i)
f(arg, arg2)
end
end
local _SOURCE_FILE_EXTS = {
[".cpp"] = true,
[".cc"] = true,
[".cxx"] = true,
[".c"] = true,
}
local function is_source_file (path)
if path[1] == "-" then
return false
end
local ext = bcache.get_extension(path):lower()
return _SOURCE_FILE_EXTS[ext]
end
local function is_dir (path)
local info = bcache.get_file_info(path)
return info["is_dir"]
end
local function find_file_in_parents (path, name)
-- Find the bottom directory.
local dir = path
if not is_dir(path) then
dir = bcache.get_dir_part(dir)
end
while #dir > 0 do
-- Check if the file exists at this directory level.
local file_path = bcache.append_path(dir, name)
if bcache.file_exists(file_path) then
return file_path
end
-- Up one level...
dir = bcache.get_dir_part(dir)
end
return nil
end
local function load_file (path)
local f = assert(io.open(path, "rb"))
local data = f:read("*all")
f:close()
return data
end
local function load_config (src_file)
-- Use the "--config=<string>" argument to find the configuration file.
local config_path = nil
for_each_arg (function (arg, arg2)
if arg == "--config" and arg2 then
config_path = bcache.remsolve_path(arg2)
end
end)
-- "When the value is empty, clang-tidy will attempt to find a file named
-- .clang-tidy for each source file in its parent directories."
if not config_path then
config_path = find_file_in_parents(src_file, ".clang-tidy")
end
local config = ""
if config_path then
config = load_file(config_path)
end
return config
end
local function load_compile_db (first_src_file)
-- Use the "-p=<path>" argument to find compile_commands.json. "For example,
-- it can be a CMake build directory in which a file named
-- compile_commands.json exists [or the path of the compile_commands.json
-- file]."
local compile_db_path = nil
for_each_arg (function (arg, arg2)
if arg == "-p" and arg2 then
compile_db_path = arg2
end
end)
if compile_db_path and is_dir(compile_db_path) then
compile_db_path = bcache.append_path(compile_db_path, "compile_commands.json")
end
-- "When no build path is specified, a search for compile_commands.json will
-- be attempted through all parent paths of the first input file."
if not compile_db_path then
compile_db_path = find_file_in_parents(first_src_file, "compile_commands.json")
end
if (not compile_db_path) or (not bcache.file_exists(compile_db_path)) then
error("No compile_commands.json file found")
end
bcache.log_debug("Found compile_commands.json: " .. compile_db_path)
-- Load the compile database.
return load_file(compile_db_path)
end
local function get_string_from_db_entry (entry, key)
-- Find the start of the string for the given key.
local start = entry:find("\"" .. key .. "\":")
if not start then
error("Key \"" .. key .. "\" not found in compilation database")
end
local pos = start + #key + 3
while pos < #entry and entry:sub(pos,pos) ~= "\"" do
pos = pos + 1
end
pos = pos + 1
-- Unescape the JSON escaped string.
local str_chars = {}
while pos < #entry and entry:sub(pos,pos) ~= "\"" do
local c = entry:sub(pos,pos)
if c == "\\" then
pos= pos + 1
c = entry:sub(pos,pos)
if c == "n" then
c = "\n"
elseif c == "r" then
c = "\r"
elseif c == "t" then
c = "\t"
end
end
table.insert(str_chars, c)
pos = pos + 1
end
return table.concat(str_chars, "")
end
local function get_compile_cmd (compile_db, src_path)
-- Find the source file entry.
local start = compile_db:find("\"" .. src_path .. "\"", nil, true)
if not start then
error("Entry for " .. src_path .. " not found in compilation database")
end
local stop = start + #src_path + 2
-- Extract the DB entry for the file: Expand start/stop until we have a full
-- JSON object node enclosed in { }.
while start > 1 and compile_db:sub(start,start) ~= "{" do
start = start - 1
end
while stop < #compile_db and compile_db:sub(stop,stop) ~= "}" do
stop = stop + 1
end
local entry = compile_db:sub(start, stop)
-- Extract the "command" field from the DB entry.
local cmd = get_string_from_db_entry(entry, "command")
local work_dir = get_string_from_db_entry (entry, "directory")
return cmd, work_dir
end
local function extract_compiler_flags (compile_args)
local flags = {}
for _, arg in ipairs(compile_args) do
local two = arg:sub(1,2)
if (two == "-D") or (two == "/D") or (two == "-U") or (two == "/U") or
(two == "-I") or (two == "/I") then
-- TODO(m): Support two-part arguments (e.g. /U foo).
if #arg < 3 then
error("Unsupported compiler flag: " .. arg)
end
table.insert(flags, "-" .. arg:sub(2))
end
end
return flags
end
local _KNOWN_GCC_PREPROCESSORS = {
"gcc",
"g++",
"clang",
}
local _KNOWN_CPP_PREPROCESSORS = {
"/usr/bin/cpp",
"/usr/bin/clang-cpp",
}
local function preprocess_src (src_path, cmd, work_dir)
-- Get all the arguments for the compiler command.
local compile_args = bcache.split_args(cmd)
pp_type = "?"
-- Check if we can use the compiler as a preprocessor. The advantages of
-- using the compiler is that we can be pretty sure that it is installed on
-- the system, and it should be able to compile the source code that we are
-- pre-processing, and hopefully it will set relevant macros etc.
local pp_path = bcache.resolve_path(compile_args[1])
local pp_name = bcache.get_file_part(pp_path:lower())
for _, name in ipairs(_KNOWN_GCC_PREPROCESSORS) do
if pp_name:find(name, nil, true) then
pp_type = "gcc"
break
end
end
-- TODO(m): Add support for cl.exe, etc?
if pp_type == "?" then
-- Try to find a known preprocessor that is installed on the system.
for _, path in ipairs(_KNOWN_CPP_PREPROCESSORS) do
-- TODO(m): We should use find_executable() here.
if bcache.file_exists(path) then
pp_path = bcache.resolve_path(path)
pp_name = bcache.get_file_part(pp_path)
pp_type = "cpp"
break
end
end
end
if pp_type == "?" then
error("Could not find a useful preprocessor")
end
bcache.log_debug("Using " .. pp_path .. " for " .. pp_type .. "-style preprocessing")
-- Construct the command line for running the preprocessor.
local preprocessor_args = extract_compiler_flags(compile_args)
table.insert(preprocessor_args, 1, pp_path)
local preprocessed_file = os.tmpname()
if pp_type == "gcc" then
table.insert(preprocessor_args, "-E")
table.insert(preprocessor_args, "-P")
table.insert(preprocessor_args, "-o")
table.insert(preprocessor_args, preprocessed_file)
table.insert(preprocessor_args, src_path)
elseif pp_type == "cpp" then
table.insert(preprocessor_args, src_path)
table.insert(preprocessor_args, "-o")
table.insert(preprocessor_args, preprocessed_file)
end
-- Run the preprocessor step.
local result = bcache.run(preprocessor_args, true, work_dir)
if result.return_code ~= 0 then
os.remove(preprocessed_file)
error("Preprocessing command was unsuccessful:\n" .. result.std_err)
end
-- Read the preprocessed file.
local preprocessed_source = load_file(preprocessed_file)
os.remove(preprocessed_file)
-- Include the preprocessor command in the result (different preprocessors
-- may produce different results).
return pp_path .. "#:#" .. preprocessed_source
end
-------------------------------------------------------------------------------
-- Wrapper interface implementation.
-------------------------------------------------------------------------------
function can_handle_command ()
-- Bail for unsupported arguments.
-- TODO(m):
-- * Add caching of "--store-check-profile=<prefix>"
for_each_arg (function (arg, arg2)
if (arg == "--fix") or
(arg == "--fix-errors") or
((arg == "--format-style") and (arg2 ~= "none")) or
(arg == "--vfsoverlay") or
(arg == "--store-check-profile")
then
error("Unsupported argument: " .. arg)
end
end)
-- Otherwise, go for it!
return true
end
function get_build_files ()
local build_files = {}
local found_fixes_file = false
for_each_arg (function (arg, arg2)
if arg == "--export-fixes" and arg2 then
if found_fixes_file then
error("Only a single --export-fixes file can be specified.")
end
build_files["fixes"] = arg2
found_fixes_file = true
end
end)
return build_files
end
function get_program_id ()
-- Get the version string for the program.
local result = bcache.run({ARGS[1], "--version"})
if result.return_code ~= 0 then
error("Unable to get the program version information string.")
end
return ARGS[1] .. ":" .. result.std_out
end
function get_relevant_arguments ()
local filtered_args = {}
-- The first argument is the compiler binary without the path.
table.insert(filtered_args, bcache.get_file_part(ARGS[1]))
for_each_arg (function (arg, arg2)
-- Ignore arguments that are handled implicitly.
local ignore_arg = ((arg == "--config") or
(arg == "--export-fixes") or
(arg == "-p"))
if not ignore_arg then
if arg2 then
table.insert(filtered_args, arg .. "=" .. arg2)
else
table.insert(filtered_args, arg)
end
end
end)
bcache.log_debug("Filtered args: " .. table.concat(filtered_args, " "))
return filtered_args
end
function preprocess_source ()
-- Collect all source files.
local src_files = {}
for_each_arg (function (arg, arg2)
if is_source_file(arg) then
table.insert(src_files, bcache.resolve_path(arg))
end
end)
if next(src_files) == nil then
error("No source files found")
end
bcache.log_debug("Source files: " .. table.concat(src_files, ", "))
-- Load the compile database to get the compiler command.
local db = load_compile_db(src_files[1])
-- Preprocess each source file.
local input_data_items = {}
for _, src_path in ipairs(src_files) do
-- Get the configuration for this file. Inlcude it as it affects the
-- results of clang-tidy.
local config = load_config(src_path)
table.insert(input_data_items, config)
-- Get the compilation command for this source file.
local cmd, work_dir = get_compile_cmd(db, src_path)
-- Preprocess the source.
table.insert(input_data_items, preprocess_src(src_path, cmd, work_dir))
end
-- Return the concatenation of all input data items.
return table.concat(input_data_items, "#:#")
end