Files
clice/src/Compiler/Tidy.cpp
2025-11-02 22:23:11 +08:00

404 lines
18 KiB
C++

//===--- Run clang-tidy ---------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
/// Partial code is copied from clangd. See:
/// https://github.com/llvm/llvm-project//blob/0865ecc5150b9a55ba1f9e30b6d463a66ac362a6/clang-tools-extra/clangd/ParsedAST.cpp#L547
/// https://github.com/llvm/llvm-project//blob/0865ecc5150b9a55ba1f9e30b6d463a66ac362a6/clang-tools-extra/clangd/TidyProvider.cpp
#include "TidyImpl.h"
#include "AST/Utility.h"
#include "Compiler/Diagnostic.h"
#include "Compiler/Tidy.h"
#include "Support/Logging.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/Allocator.h"
#include "llvm/Support/Process.h"
#include "llvm/Support/StringSaver.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang-tidy/ClangTidyOptions.h"
#include "clang-tidy/ClangTidyCheck.h"
#include "clang-tidy/ClangTidyModuleRegistry.h"
#include "clang-tidy/ClangTidyDiagnosticConsumer.h"
#define CLANG_TIDY_DISABLE_STATIC_ANALYZER_CHECKS
#include "clang-tidy/ClangTidyForceLinker.h"
namespace clice::tidy {
using namespace clang::tidy;
bool is_registered_tidy_check(llvm::StringRef check) {
assert(!check.empty());
assert(!check.contains('*') && !check.contains(',') &&
"is_registered_tidy_check doesn't support globs");
assert(check.ltrim().front() != '-');
const static llvm::StringSet<llvm::BumpPtrAllocator> all_checks = [] {
llvm::StringSet<llvm::BumpPtrAllocator> result;
tidy::ClangTidyCheckFactories factories;
for(tidy::ClangTidyModuleRegistry::entry entry: tidy::ClangTidyModuleRegistry::entries())
entry.instantiate()->addCheckFactories(factories);
for(const auto& factory: factories)
result.insert(factory.getKey());
return result;
}();
return all_checks.contains(check);
}
std::optional<bool> is_fast_tidy_check(llvm::StringRef check) {
static auto fast = llvm::StringMap<bool>{
#define FAST(CHECK, TIME) {#CHECK, true},
#define SLOW(CHECK, TIME) {#CHECK, false},
// todo: move me to llvm toolchain headers.
#include "TidyFastChecks.inc"
};
if(auto it = fast.find(check); it != fast.end()) {
return it->second;
}
return std::nullopt;
}
tidy::ClangTidyCheckFactories get_fast_checks(const tidy::ClangTidyCheckFactories& all) {
tidy::ClangTidyCheckFactories fast;
for(const auto& factory: all) {
if(is_fast_tidy_check(factory.getKey()).value_or(false)) {
fast.registerCheckFactory(factory.first(), factory.second);
}
}
return fast;
}
tidy::ClangTidyOptions create_options() {
// getDefaults instantiates all check factories, which are registered at link
// time. So cache the results once.
const static auto default_opts = [] {
auto opts = tidy::ClangTidyOptions::getDefaults();
opts.Checks->clear();
return opts;
}();
// These default checks are chosen for:
// - low false-positive rate
// - providing a lot of value
// - being reasonably efficient
const static std::string default_checks = llvm::join_items(",",
"readability-misleading-indentation",
"readability-deleted-default",
"bugprone-integer-division",
"bugprone-sizeof-expression",
"bugprone-suspicious-missing-comma",
"bugprone-unused-raii",
"bugprone-unused-return-value",
"misc-unused-using-decls",
"misc-unused-alias-decls",
"misc-definitions-in-headers");
const static std::string bad_checks =
llvm::join_items(",",
// We want this list to start with a separator to
// simplify appending in the lambda. So including an
// empty string here will force that.
"",
// include-cleaner is directly integrated in IncludeCleaner.cpp
"-misc-include-cleaner",
// ----- False Positives -----
// Check relies on seeing ifndef/define/endif directives,
// clangd doesn't replay those when using a preamble.
"-llvm-header-guard",
"-modernize-macro-to-enum",
// ----- Crashing Checks -----
// Check can choke on invalid (intermediate) c++
// code, which is often the case when clangd
// tries to build an AST.
"-bugprone-use-after-move",
// Alias for bugprone-use-after-move.
"-hicpp-invalid-access-moved",
// Check uses dataflow analysis, which might hang/crash unexpectedly on
// incomplete code.
"-bugprone-unchecked-optional-access");
tidy::ClangTidyOptions opts = default_opts;
// clang::clangd::provideEnvironment
if(std::optional<std::string> user = llvm::sys::Process::GetEnv("USER")) {
opts.User = user;
}
// TODO: Providers.push_back(provideClangTidyFiles(TFS)); Filename
// TODO: if(EnableConfig) Providers.push_back(provideClangdConfig());
// clang::clangd::provideDefaultChecks
if(!opts.Checks || opts.Checks->empty()) {
opts.Checks = default_checks;
}
// clang::clangd::disableUnusableChecks
if(opts.Checks && !opts.Checks->empty()) {
opts.Checks->append(bad_checks);
}
return opts;
}
// Filter for clang diagnostics groups enabled by CTOptions.Checks.
//
// These are check names like clang-diagnostics-unused.
// Note that unlike -Wunused, clang-diagnostics-unused does not imply
// subcategories like clang-diagnostics-unused-function.
//
// This is used to determine which diagnostics can be enabled by ExtraArgs in
// the clang-tidy configuration.
class TidyDiagnosticGroups {
// Whether all diagnostic groups are enabled by default.
// True if we've seen clang-diagnostic-*.
bool default_enable = false;
// Set of diag::Group whose enablement != default_enable.
// If default_enable is false, this is foo where we've seen clang-diagnostic-foo.
llvm::DenseSet<unsigned> exceptions;
public:
TidyDiagnosticGroups(llvm::StringRef checks) {
constexpr llvm::StringLiteral CDPrefix = "clang-diagnostic-";
llvm::StringRef check;
while(!checks.empty()) {
std::tie(check, checks) = checks.split(',');
check = check.trim();
if(check.empty()) {
continue;
}
bool enable = !check.consume_front("-");
bool glob = check.consume_back("*");
if(glob) {
// Is this clang-diagnostic-*, or *, or so?
// (We ignore all other types of globs).
if(CDPrefix.starts_with(check)) {
default_enable = enable;
exceptions.clear();
}
continue;
}
// In "*,clang-diagnostic-foo", the latter is a no-op.
if(default_enable == enable) {
continue;
}
// The only non-glob entries we care about are clang-diagnostic-foo.
if(!check.consume_front(CDPrefix)) {
continue;
}
if(auto group = clang::DiagnosticIDs::getGroupForWarningOption(check)) {
exceptions.insert(static_cast<unsigned>(*group));
}
}
}
bool operator() (clang::diag::Group group_id) const {
return exceptions.contains(static_cast<unsigned>(group_id)) ? !default_enable
: default_enable;
}
};
// Find -W<group> and -Wno-<group> options in extra_args and apply them to diags.
//
// This is used to handle extra_args in clang-tidy configuration.
// We don't use clang's standard handling of this as we want slightly different
// behavior (e.g. we want to exclude these from -Wno-error).
void apply_warning_options(llvm::ArrayRef<std::string> extra_args,
llvm::function_ref<bool(clang::diag::Group)> enable_groups,
clang::DiagnosticsEngine& diags) {
for(llvm::StringRef group: extra_args) {
// Only handle args that are of the form -W[no-]<group>.
// Other flags are possible but rare and deliberately out of scope.
llvm::SmallVector<clang::diag::kind> members;
if(!group.consume_front("-W") || group.empty()) {
continue;
}
bool enable = !group.consume_front("no-");
if(diags.getDiagnosticIDs()->getDiagnosticsInGroup(clang::diag::Flavor::WarningOrError,
group,
members)) {
continue;
}
// Upgrade (or downgrade) the severity of each diagnostic in the group.
// If -Werror is on, newly added warnings will be treated as errors.
// We don't want this, so keep track of them to fix afterwards.
bool needs_werror_exclusion = false;
for(clang::diag::kind id: members) {
if(enable) {
if(diags.getDiagnosticLevel(id, clang::SourceLocation()) <
clang::DiagnosticsEngine::Warning) {
auto group = diags.getDiagnosticIDs()->getGroupForDiag(id);
if(!group || !enable_groups(*group)) {
continue;
}
diags.setSeverity(id, clang::diag::Severity::Warning, clang::SourceLocation());
if(diags.getWarningsAsErrors()) {
needs_werror_exclusion = true;
}
}
} else {
diags.setSeverity(id, clang::diag::Severity::Ignored, clang::SourceLocation());
}
}
if(needs_werror_exclusion) {
// FIXME: there's no API to suppress -Werror for single diagnostics.
// In some cases with sub-groups, we may end up erroneously
// downgrading diagnostics that were -Werror in the compile command.
diags.setDiagnosticGroupWarningAsError(group, false);
}
}
}
ClangTidyChecker::ClangTidyChecker(std::unique_ptr<ClangTidyOptionsProvider> provider) :
context(std::move(provider)) {}
clang::DiagnosticsEngine::Level
ClangTidyChecker::adjust_level(clang::DiagnosticsEngine::Level level,
const clang::Diagnostic& diag) {
if(!checks.empty()) {
std::string tidy_diag = context.getCheckName(diag.getID());
bool is_clang_tidy_diag = !tidy_diag.empty();
if(is_clang_tidy_diag) {
// Check for suppression comment. Skip the check for diagnostics not
// in the main file, because we don't want that function to query the
// source buffer for preamble files. For the same reason, we ask
// shouldSuppressDiagnostic to avoid I/O.
// We let suppression comments take precedence over warning-as-error
// to match clang-tidy's behaviour.
bool in_main_file =
diag.hasSourceManager() &&
ast::is_inside_main_file(diag.getLocation(), diag.getSourceManager());
llvm::SmallVector<clang::tooling::Diagnostic, 1> tidy_suppressed_errors;
if(in_main_file && context.shouldSuppressDiagnostic(level,
diag,
tidy_suppressed_errors,
/*AllowIO=*/false,
/*EnableNolintBlocks=*/true)) {
// FIXME: should we expose the suppression error (invalid use of
// NOLINT comments)?
return clang::DiagnosticsEngine::Ignored;
}
if(!context.getOptions().SystemHeaders.value_or(false) && diag.hasSourceManager() &&
diag.getSourceManager().isInSystemMacro(diag.getLocation())) {
return clang::DiagnosticsEngine::Ignored;
}
// Check for warning-as-error.
if(level == clang::DiagnosticsEngine::Warning && context.treatAsError(tidy_diag)) {
return clang::DiagnosticsEngine::Error;
}
}
}
return level;
}
void ClangTidyChecker::adjust_diag(Diagnostic& diag) {
std::string tidy_diag = context.getCheckName(diag.id.value);
if(!tidy_diag.empty()) {
// TODO: using a global string saver.
static llvm::BumpPtrAllocator allocator;
static llvm::StringSaver saver(allocator);
diag.id.name = saver.save(tidy_diag);
diag.id.source = DiagnosticSource::ClangTidy;
// clang-tidy bakes the name into diagnostic messages. Strip it out.
// It would be much nicer to make clang-tidy not do this.
auto clean_message = [&](std::string& msg) {
llvm::StringRef rest(msg);
if(rest.consume_back("]") && rest.consume_back(diag.id.name) && rest.consume_back(" ["))
msg.resize(rest.size());
};
clean_message(diag.message);
// todo: where is clice notes and fixes?
// for(auto& note: diag.Notes)
// clean_message(note.Message);
// for(auto& fix: diag.Fixes)
// clean_message(fix.Message);
}
}
std::unique_ptr<ClangTidyChecker> configure(clang::CompilerInstance& instance,
const TidyParams& params) {
auto& input = instance.getFrontendOpts().Inputs[0];
if(!input.isFile()) {
return nullptr;
}
auto file_name = input.getFile();
logging::info("Tidy configure file: {}", file_name);
tidy::ClangTidyOptions opts = create_options();
if(opts.Checks) {
logging::info("Tidy configure checks: {}", *opts.Checks);
}
{
// If clang-tidy is configured to emit clang warnings, we should too.
//
// Such clang-tidy configuration consists of two parts:
// - ExtraArgs: ["-Wfoo"] causes clang to produce the warnings
// - Checks: "clang-diagnostic-foo" prevents clang-tidy filtering them out
//
// In clang-tidy, diagnostics are emitted if they pass both checks.
// When groups contain subgroups, -Wparent includes the child, but
// clang-diagnostic-parent does not.
//
// We *don't* want to change the compile command directly. This can have
// too many unexpected effects: breaking the command, interactions with
// -- and -Werror, etc. Besides, we've already parsed the command.
// Instead we parse the -W<group> flags and handle them directly.
//
// Similarly, we don't want to use Checks to filter clang diagnostics after
// they are generated, as this spreads clang-tidy emulation everywhere.
// Instead, we just use these to filter which extra diagnostics we enable.
auto& diags = instance.getDiagnostics();
TidyDiagnosticGroups groups(opts.Checks ? *opts.Checks : llvm::StringRef());
if(opts.ExtraArgsBefore) {
apply_warning_options(*opts.ExtraArgsBefore, groups, diags);
}
if(opts.ExtraArgs) {
apply_warning_options(*opts.ExtraArgs, groups, diags);
}
}
/// No need to run clang-tidy or IncludeFixerif we are not going to surface
/// diagnostics.
const static auto all_factories = [] {
tidy::ClangTidyCheckFactories factories;
for(const auto& e: tidy::ClangTidyModuleRegistry::entries()) {
e.instantiate()->addCheckFactories(factories);
}
return factories;
}();
tidy::ClangTidyCheckFactories factories = get_fast_checks(all_factories);
std::unique_ptr<ClangTidyChecker> checker = std::make_unique<ClangTidyChecker>(
std::make_unique<tidy::DefaultOptionsProvider>(tidy::ClangTidyGlobalOptions(), opts));
checker->context.setDiagnosticsEngine(
std::make_unique<clang::DiagnosticOptions>(instance.getDiagnosticOpts()),
&instance.getDiagnostics());
checker->context.setASTContext(&instance.getASTContext());
// TODO: is `file_name` always the file to check?
checker->context.setCurrentFile(file_name);
checker->context.setSelfContainedDiags(true);
checker->checks = factories.createChecksForLanguage(&checker->context);
logging::info("Tidy configure checks: {}", checker->checks.size());
clang::Preprocessor* pp = &instance.getPreprocessor();
for(const auto& check: checker->checks) {
check->registerPPCallbacks(instance.getSourceManager(), pp, pp);
check->registerMatchers(&checker->finder);
}
return checker;
}
} // namespace clice::tidy