1 //===--- TidyProvider.cpp - create options for running clang-tidy----------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 
9 #include "TidyProvider.h"
10 #include "../clang-tidy/ClangTidyModuleRegistry.h"
11 #include "Config.h"
12 #include "support/FileCache.h"
13 #include "support/Logger.h"
14 #include "support/Path.h"
15 #include "support/ThreadsafeFS.h"
16 #include "llvm/ADT/STLExtras.h"
17 #include "llvm/ADT/SmallString.h"
18 #include "llvm/ADT/StringExtras.h"
19 #include "llvm/ADT/StringSet.h"
20 #include "llvm/Support/Allocator.h"
21 #include "llvm/Support/Process.h"
22 #include "llvm/Support/SourceMgr.h"
23 #include <memory>
24 
25 namespace clang {
26 namespace clangd {
27 namespace {
28 
29 // Access to config from a .clang-tidy file, caching IO and parsing.
30 class DotClangTidyCache : private FileCache {
31   // We cache and expose shared_ptr to avoid copying the value on every lookup
32   // when we're ultimately just going to pass it to mergeWith.
33   mutable std::shared_ptr<const tidy::ClangTidyOptions> Value;
34 
35 public:
DotClangTidyCache(PathRef Path)36   DotClangTidyCache(PathRef Path) : FileCache(Path) {}
37 
38   std::shared_ptr<const tidy::ClangTidyOptions>
get(const ThreadsafeFS & TFS,std::chrono::steady_clock::time_point FreshTime) const39   get(const ThreadsafeFS &TFS,
40       std::chrono::steady_clock::time_point FreshTime) const {
41     std::shared_ptr<const tidy::ClangTidyOptions> Result;
42     read(
43         TFS, FreshTime,
44         [this](llvm::Optional<llvm::StringRef> Data) {
45           Value.reset();
46           if (Data && !Data->empty()) {
47             tidy::DiagCallback Diagnostics = [](const llvm::SMDiagnostic &D) {
48               switch (D.getKind()) {
49               case llvm::SourceMgr::DK_Error:
50                 elog("tidy-config error at {0}:{1}:{2}: {3}", D.getFilename(),
51                      D.getLineNo(), D.getColumnNo(), D.getMessage());
52                 break;
53               case llvm::SourceMgr::DK_Warning:
54                 log("tidy-config warning at {0}:{1}:{2}: {3}", D.getFilename(),
55                     D.getLineNo(), D.getColumnNo(), D.getMessage());
56                 break;
57               case llvm::SourceMgr::DK_Note:
58               case llvm::SourceMgr::DK_Remark:
59                 vlog("tidy-config note at {0}:{1}:{2}: {3}", D.getFilename(),
60                      D.getLineNo(), D.getColumnNo(), D.getMessage());
61                 break;
62               }
63             };
64             if (auto Parsed = tidy::parseConfigurationWithDiags(
65                     llvm::MemoryBufferRef(*Data, path()), Diagnostics))
66               Value = std::make_shared<const tidy::ClangTidyOptions>(
67                   std::move(*Parsed));
68             else
69               elog("Error parsing clang-tidy configuration in {0}: {1}", path(),
70                    Parsed.getError().message());
71           }
72         },
73         [&]() { Result = Value; });
74     return Result;
75   }
76 };
77 
78 // Access to combined config from .clang-tidy files governing a source file.
79 // Each config file is cached and the caches are shared for affected sources.
80 //
81 // FIXME: largely duplicates config::Provider::fromAncestorRelativeYAMLFiles.
82 // Potentially useful for compile_commands.json too. Extract?
83 class DotClangTidyTree {
84   const ThreadsafeFS &FS;
85   std::string RelPath;
86   std::chrono::steady_clock::duration MaxStaleness;
87 
88   mutable std::mutex Mu;
89   // Keys are the ancestor directory, not the actual config path within it.
90   // We only insert into this map, so pointers to values are stable forever.
91   // Mutex guards the map itself, not the values (which are threadsafe).
92   mutable llvm::StringMap<DotClangTidyCache> Cache;
93 
94 public:
DotClangTidyTree(const ThreadsafeFS & FS)95   DotClangTidyTree(const ThreadsafeFS &FS)
96       : FS(FS), RelPath(".clang-tidy"), MaxStaleness(std::chrono::seconds(5)) {}
97 
apply(tidy::ClangTidyOptions & Result,PathRef AbsPath)98   void apply(tidy::ClangTidyOptions &Result, PathRef AbsPath) {
99     namespace path = llvm::sys::path;
100     assert(path::is_absolute(AbsPath));
101 
102     // Compute absolute paths to all ancestors (substrings of P.Path).
103     // Ensure cache entries for each ancestor exist in the map.
104     llvm::SmallVector<DotClangTidyCache *> Caches;
105     {
106       std::lock_guard<std::mutex> Lock(Mu);
107       for (auto Ancestor = absoluteParent(AbsPath); !Ancestor.empty();
108            Ancestor = absoluteParent(Ancestor)) {
109         auto It = Cache.find(Ancestor);
110         // Assemble the actual config file path only if needed.
111         if (It == Cache.end()) {
112           llvm::SmallString<256> ConfigPath = Ancestor;
113           path::append(ConfigPath, RelPath);
114           It = Cache.try_emplace(Ancestor, ConfigPath.str()).first;
115         }
116         Caches.push_back(&It->second);
117       }
118     }
119     // Finally query each individual file.
120     // This will take a (per-file) lock for each file that actually exists.
121     std::chrono::steady_clock::time_point FreshTime =
122         std::chrono::steady_clock::now() - MaxStaleness;
123     llvm::SmallVector<std::shared_ptr<const tidy::ClangTidyOptions>>
124         OptionStack;
125     for (const DotClangTidyCache *Cache : Caches)
126       if (auto Config = Cache->get(FS, FreshTime)) {
127         OptionStack.push_back(std::move(Config));
128         if (!OptionStack.back()->InheritParentConfig.value_or(false))
129           break;
130       }
131     unsigned Order = 1u;
132     for (auto &Option : llvm::reverse(OptionStack))
133       Result.mergeWith(*Option, Order++);
134   }
135 };
136 
137 } // namespace
138 
mergeCheckList(llvm::Optional<std::string> & Checks,llvm::StringRef List)139 static void mergeCheckList(llvm::Optional<std::string> &Checks,
140                            llvm::StringRef List) {
141   if (List.empty())
142     return;
143   if (!Checks || Checks->empty()) {
144     Checks.emplace(List);
145     return;
146   }
147   *Checks = llvm::join_items(",", *Checks, List);
148 }
149 
provideEnvironment()150 TidyProviderRef provideEnvironment() {
151   static const llvm::Optional<std::string> User = [] {
152     llvm::Optional<std::string> Ret = llvm::sys::Process::GetEnv("USER");
153 #ifdef _WIN32
154     if (!Ret)
155       return llvm::sys::Process::GetEnv("USERNAME");
156 #endif
157     return Ret;
158   }();
159 
160   if (User)
161     return
162         [](tidy::ClangTidyOptions &Opts, llvm::StringRef) { Opts.User = User; };
163   // FIXME: Once function_ref and unique_function operator= operators handle
164   // null values, this can return null.
165   return [](tidy::ClangTidyOptions &, llvm::StringRef) {};
166 }
167 
provideDefaultChecks()168 TidyProviderRef provideDefaultChecks() {
169   // These default checks are chosen for:
170   //  - low false-positive rate
171   //  - providing a lot of value
172   //  - being reasonably efficient
173   static const std::string DefaultChecks = llvm::join_items(
174       ",", "readability-misleading-indentation", "readability-deleted-default",
175       "bugprone-integer-division", "bugprone-sizeof-expression",
176       "bugprone-suspicious-missing-comma", "bugprone-unused-raii",
177       "bugprone-unused-return-value", "misc-unused-using-decls",
178       "misc-unused-alias-decls", "misc-definitions-in-headers");
179   return [](tidy::ClangTidyOptions &Opts, llvm::StringRef) {
180     if (!Opts.Checks || Opts.Checks->empty())
181       Opts.Checks = DefaultChecks;
182   };
183 }
184 
addTidyChecks(llvm::StringRef Checks,llvm::StringRef WarningsAsErrors)185 TidyProvider addTidyChecks(llvm::StringRef Checks,
186                            llvm::StringRef WarningsAsErrors) {
187   return [Checks = std::string(Checks),
188           WarningsAsErrors = std::string(WarningsAsErrors)](
189              tidy::ClangTidyOptions &Opts, llvm::StringRef) {
190     mergeCheckList(Opts.Checks, Checks);
191     mergeCheckList(Opts.WarningsAsErrors, WarningsAsErrors);
192   };
193 }
194 
disableUnusableChecks(llvm::ArrayRef<std::string> ExtraBadChecks)195 TidyProvider disableUnusableChecks(llvm::ArrayRef<std::string> ExtraBadChecks) {
196   constexpr llvm::StringLiteral Seperator(",");
197   static const std::string BadChecks =
198       llvm::join_items(Seperator,
199                        // We want this list to start with a seperator to
200                        // simplify appending in the lambda. So including an
201                        // empty string here will force that.
202                        "",
203                        // ----- False Positives -----
204 
205                        // Check relies on seeing ifndef/define/endif directives,
206                        // clangd doesn't replay those when using a preamble.
207                        "-llvm-header-guard",
208 
209                        // ----- Crashing Checks -----
210 
211                        // Check can choke on invalid (intermediate) c++
212                        // code, which is often the case when clangd
213                        // tries to build an AST.
214                        "-bugprone-use-after-move",
215                        // Alias for bugprone-use-after-move.
216                        "-hicpp-invalid-access-moved",
217 
218                        // ----- Performance problems -----
219 
220                        // This check runs expensive analysis for each variable.
221                        // It has been observed to increase reparse time by 10x.
222                        "-misc-const-correctness");
223 
224   size_t Size = BadChecks.size();
225   for (const std::string &Str : ExtraBadChecks) {
226     if (Str.empty())
227       continue;
228     Size += Seperator.size();
229     if (LLVM_LIKELY(Str.front() != '-'))
230       ++Size;
231     Size += Str.size();
232   }
233   std::string DisableGlob;
234   DisableGlob.reserve(Size);
235   DisableGlob += BadChecks;
236   for (const std::string &Str : ExtraBadChecks) {
237     if (Str.empty())
238       continue;
239     DisableGlob += Seperator;
240     if (LLVM_LIKELY(Str.front() != '-'))
241       DisableGlob.push_back('-');
242     DisableGlob += Str;
243   }
244 
245   return [DisableList(std::move(DisableGlob))](tidy::ClangTidyOptions &Opts,
246                                                llvm::StringRef) {
247     if (Opts.Checks && !Opts.Checks->empty())
248       Opts.Checks->append(DisableList);
249   };
250 }
251 
provideClangdConfig()252 TidyProviderRef provideClangdConfig() {
253   return [](tidy::ClangTidyOptions &Opts, llvm::StringRef) {
254     const auto &CurTidyConfig = Config::current().Diagnostics.ClangTidy;
255     if (!CurTidyConfig.Checks.empty())
256       mergeCheckList(Opts.Checks, CurTidyConfig.Checks);
257 
258     for (const auto &CheckOption : CurTidyConfig.CheckOptions)
259       Opts.CheckOptions.insert_or_assign(CheckOption.getKey(),
260                                          tidy::ClangTidyOptions::ClangTidyValue(
261                                              CheckOption.getValue(), 10000U));
262   };
263 }
264 
provideClangTidyFiles(ThreadsafeFS & TFS)265 TidyProvider provideClangTidyFiles(ThreadsafeFS &TFS) {
266   return [Tree = std::make_unique<DotClangTidyTree>(TFS)](
267              tidy::ClangTidyOptions &Opts, llvm::StringRef Filename) {
268     Tree->apply(Opts, Filename);
269   };
270 }
271 
combine(std::vector<TidyProvider> Providers)272 TidyProvider combine(std::vector<TidyProvider> Providers) {
273   // FIXME: Once function_ref and unique_function operator= operators handle
274   // null values, we should filter out any Providers that are null. Right now we
275   // have to ensure we dont pass any providers that are null.
276   return [Providers(std::move(Providers))](tidy::ClangTidyOptions &Opts,
277                                            llvm::StringRef Filename) {
278     for (const auto &Provider : Providers)
279       Provider(Opts, Filename);
280   };
281 }
282 
getTidyOptionsForFile(TidyProviderRef Provider,llvm::StringRef Filename)283 tidy::ClangTidyOptions getTidyOptionsForFile(TidyProviderRef Provider,
284                                              llvm::StringRef Filename) {
285   tidy::ClangTidyOptions Opts = tidy::ClangTidyOptions::getDefaults();
286   Opts.Checks->clear();
287   if (Provider)
288     Provider(Opts, Filename);
289   return Opts;
290 }
291 
isRegisteredTidyCheck(llvm::StringRef Check)292 bool isRegisteredTidyCheck(llvm::StringRef Check) {
293   assert(!Check.empty());
294   assert(!Check.contains('*') && !Check.contains(',') &&
295          "isRegisteredCheck doesn't support globs");
296   assert(Check.ltrim().front() != '-');
297 
298   static const llvm::StringSet<llvm::BumpPtrAllocator> AllChecks = [] {
299     llvm::StringSet<llvm::BumpPtrAllocator> Result;
300     tidy::ClangTidyCheckFactories Factories;
301     for (tidy::ClangTidyModuleRegistry::entry E :
302          tidy::ClangTidyModuleRegistry::entries())
303       E.instantiate()->addCheckFactories(Factories);
304     for (const auto &Factory : Factories)
305       Result.insert(Factory.getKey());
306     return Result;
307   }();
308 
309   return AllChecks.contains(Check);
310 }
311 } // namespace clangd
312 } // namespace clang
313