1//===--- ClangTidyOptions.cpp - clang-tidy ----------------------*- C++ -*-===//
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 "ClangTidyOptions.h"
10#include "ClangTidyModuleRegistry.h"
11#include "clang/Basic/LLVM.h"
12#include "llvm/ADT/SmallString.h"
13#include "llvm/Support/Debug.h"
14#include "llvm/Support/Errc.h"
15#include "llvm/Support/FileSystem.h"
16#include "llvm/Support/MemoryBufferRef.h"
17#include "llvm/Support/Path.h"
18#include "llvm/Support/YAMLTraits.h"
19#include <optional>
20#include <utility>
21
22#define DEBUG_TYPE "clang-tidy-options"
23
24using clang::tidy::ClangTidyOptions;
25using clang::tidy::FileFilter;
26using OptionsSource = clang::tidy::ClangTidyOptionsProvider::OptionsSource;
27
28LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(FileFilter)
29LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(FileFilter::LineRange)
30
31namespace llvm::yaml {
32
33// Map std::pair<int, int> to a JSON array of size 2.
34template <> struct SequenceTraits<FileFilter::LineRange> {
35 static size_t size(IO &IO, FileFilter::LineRange &Range) {
36 return Range.first == 0 ? 0 : Range.second == 0 ? 1 : 2;
37 }
38 static unsigned &element(IO &IO, FileFilter::LineRange &Range, size_t Index) {
39 if (Index > 1)
40 IO.setError("Too many elements in line range.");
41 return Index == 0 ? Range.first : Range.second;
42 }
43};
44
45template <> struct MappingTraits<FileFilter> {
46 static void mapping(IO &IO, FileFilter &File) {
47 IO.mapRequired("name", File.Name);
48 IO.mapOptional("lines", File.LineRanges);
49 }
50 static std::string validate(IO &Io, FileFilter &File) {
51 if (File.Name.empty())
52 return "No file name specified";
53 for (const FileFilter::LineRange &Range : File.LineRanges) {
54 if (Range.first <= 0 || Range.second <= 0)
55 return "Invalid line range";
56 }
57 return "";
58 }
59};
60
61template <> struct MappingTraits<ClangTidyOptions::StringPair> {
62 static void mapping(IO &IO, ClangTidyOptions::StringPair &KeyValue) {
63 IO.mapRequired("key", KeyValue.first);
64 IO.mapRequired("value", KeyValue.second);
65 }
66};
67
68struct NOptionMap {
69 NOptionMap(IO &) {}
70 NOptionMap(IO &, const ClangTidyOptions::OptionMap &OptionMap) {
71 Options.reserve(OptionMap.size());
72 for (const auto &KeyValue : OptionMap)
73 Options.emplace_back(std::string(KeyValue.getKey()), KeyValue.getValue().Value);
74 }
75 ClangTidyOptions::OptionMap denormalize(IO &) {
76 ClangTidyOptions::OptionMap Map;
77 for (const auto &KeyValue : Options)
78 Map[KeyValue.first] = ClangTidyOptions::ClangTidyValue(KeyValue.second);
79 return Map;
80 }
81 std::vector<ClangTidyOptions::StringPair> Options;
82};
83
84template <>
85void yamlize(IO &IO, ClangTidyOptions::OptionMap &Options, bool,
86 EmptyContext &Ctx) {
87 if (IO.outputting()) {
88 IO.beginMapping();
89 // Only output as a map
90 for (auto &Key : Options) {
91 bool UseDefault;
92 void *SaveInfo;
93 IO.preflightKey(Key.getKey().data(), true, false, UseDefault, SaveInfo);
94 StringRef S = Key.getValue().Value;
95 IO.scalarString(S, needsQuotes(S));
96 IO.postflightKey(SaveInfo);
97 }
98 IO.endMapping();
99 } else {
100 // We need custom logic here to support the old method of specifying check
101 // options using a list of maps containing key and value keys.
102 Input &I = reinterpret_cast<Input &>(IO);
103 if (isa<SequenceNode>(I.getCurrentNode())) {
104 MappingNormalization<NOptionMap, ClangTidyOptions::OptionMap> NOpts(
105 IO, Options);
106 EmptyContext Ctx;
107 yamlize(IO, NOpts->Options, true, Ctx);
108 } else if (isa<MappingNode>(I.getCurrentNode())) {
109 IO.beginMapping();
110 for (StringRef Key : IO.keys()) {
111 IO.mapRequired(Key.data(), Options[Key].Value);
112 }
113 IO.endMapping();
114 } else {
115 IO.setError("expected a sequence or map");
116 }
117 }
118}
119
120struct ChecksVariant {
121 std::optional<std::string> AsString;
122 std::optional<std::vector<std::string>> AsVector;
123};
124
125template <>
126void yamlize(IO &IO, ChecksVariant &Checks, bool, EmptyContext &Ctx) {
127 if (!IO.outputting()) {
128 // Special case for reading from YAML
129 // Must support reading from both a string or a list
130 Input &I = reinterpret_cast<Input &>(IO);
131 if (isa<ScalarNode, BlockScalarNode>(I.getCurrentNode())) {
132 Checks.AsString = std::string();
133 yamlize(IO, *Checks.AsString, true, Ctx);
134 } else if (isa<SequenceNode>(I.getCurrentNode())) {
135 Checks.AsVector = std::vector<std::string>();
136 yamlize(IO, *Checks.AsVector, true, Ctx);
137 } else {
138 IO.setError("expected string or sequence");
139 }
140 }
141}
142
143static void mapChecks(IO &IO, std::optional<std::string> &Checks) {
144 if (IO.outputting()) {
145 // Output always a string
146 IO.mapOptional("Checks", Checks);
147 } else {
148 // Input as either a string or a list
149 ChecksVariant ChecksAsVariant;
150 IO.mapOptional("Checks", ChecksAsVariant);
151 if (ChecksAsVariant.AsString)
152 Checks = ChecksAsVariant.AsString;
153 else if (ChecksAsVariant.AsVector)
154 Checks = llvm::join(*ChecksAsVariant.AsVector, ",");
155 }
156}
157
158template <> struct MappingTraits<ClangTidyOptions> {
159 static void mapping(IO &IO, ClangTidyOptions &Options) {
160 bool Ignored = false;
161 mapChecks(IO, Options.Checks);
162 IO.mapOptional("WarningsAsErrors", Options.WarningsAsErrors);
163 IO.mapOptional("HeaderFileExtensions", Options.HeaderFileExtensions);
164 IO.mapOptional("ImplementationFileExtensions",
165 Options.ImplementationFileExtensions);
166 IO.mapOptional("HeaderFilterRegex", Options.HeaderFilterRegex);
167 IO.mapOptional("AnalyzeTemporaryDtors", Ignored); // deprecated
168 IO.mapOptional("FormatStyle", Options.FormatStyle);
169 IO.mapOptional("User", Options.User);
170 IO.mapOptional("CheckOptions", Options.CheckOptions);
171 IO.mapOptional("ExtraArgs", Options.ExtraArgs);
172 IO.mapOptional("ExtraArgsBefore", Options.ExtraArgsBefore);
173 IO.mapOptional("InheritParentConfig", Options.InheritParentConfig);
174 IO.mapOptional("UseColor", Options.UseColor);
175 IO.mapOptional("SystemHeaders", Options.SystemHeaders);
176 }
177};
178
179} // namespace llvm::yaml
180
181namespace clang::tidy {
182
183ClangTidyOptions ClangTidyOptions::getDefaults() {
184 ClangTidyOptions Options;
185 Options.Checks = "";
186 Options.WarningsAsErrors = "";
187 Options.HeaderFileExtensions = {"", "h", "hh", "hpp", "hxx"};
188 Options.ImplementationFileExtensions = {"c", "cc", "cpp", "cxx"};
189 Options.HeaderFilterRegex = "";
190 Options.SystemHeaders = false;
191 Options.FormatStyle = "none";
192 Options.User = std::nullopt;
193 for (const ClangTidyModuleRegistry::entry &Module :
194 ClangTidyModuleRegistry::entries())
195 Options.mergeWith(Module.instantiate()->getModuleOptions(), 0);
196 return Options;
197}
198
199template <typename T>
200static void mergeVectors(std::optional<T> &Dest, const std::optional<T> &Src) {
201 if (Src) {
202 if (Dest)
203 Dest->insert(Dest->end(), Src->begin(), Src->end());
204 else
205 Dest = Src;
206 }
207}
208
209static void mergeCommaSeparatedLists(std::optional<std::string> &Dest,
210 const std::optional<std::string> &Src) {
211 if (Src)
212 Dest = (Dest && !Dest->empty() ? *Dest + "," : "") + *Src;
213}
214
215template <typename T>
216static void overrideValue(std::optional<T> &Dest, const std::optional<T> &Src) {
217 if (Src)
218 Dest = Src;
219}
220
221ClangTidyOptions &ClangTidyOptions::mergeWith(const ClangTidyOptions &Other,
222 unsigned Order) {
223 mergeCommaSeparatedLists(Checks, Other.Checks);
224 mergeCommaSeparatedLists(WarningsAsErrors, Other.WarningsAsErrors);
225 overrideValue(HeaderFileExtensions, Other.HeaderFileExtensions);
226 overrideValue(ImplementationFileExtensions,
227 Other.ImplementationFileExtensions);
228 overrideValue(HeaderFilterRegex, Other.HeaderFilterRegex);
229 overrideValue(SystemHeaders, Other.SystemHeaders);
230 overrideValue(FormatStyle, Other.FormatStyle);
231 overrideValue(User, Other.User);
232 overrideValue(UseColor, Other.UseColor);
233 mergeVectors(ExtraArgs, Other.ExtraArgs);
234 mergeVectors(ExtraArgsBefore, Other.ExtraArgsBefore);
235
236 for (const auto &KeyValue : Other.CheckOptions) {
237 CheckOptions.insert_or_assign(
238 KeyValue.getKey(),
239 ClangTidyValue(KeyValue.getValue().Value,
240 KeyValue.getValue().Priority + Order));
241 }
242 return *this;
243}
244
245ClangTidyOptions ClangTidyOptions::merge(const ClangTidyOptions &Other,
246 unsigned Order) const {
247 ClangTidyOptions Result = *this;
248 Result.mergeWith(Other, Order);
249 return Result;
250}
251
252const char ClangTidyOptionsProvider::OptionsSourceTypeDefaultBinary[] =
253 "clang-tidy binary";
254const char ClangTidyOptionsProvider::OptionsSourceTypeCheckCommandLineOption[] =
255 "command-line option '-checks'";
256const char
257 ClangTidyOptionsProvider::OptionsSourceTypeConfigCommandLineOption[] =
258 "command-line option '-config'";
259
260ClangTidyOptions
261ClangTidyOptionsProvider::getOptions(llvm::StringRef FileName) {
262 ClangTidyOptions Result;
263 unsigned Priority = 0;
264 for (auto &Source : getRawOptions(FileName))
265 Result.mergeWith(Source.first, ++Priority);
266 return Result;
267}
268
269std::vector<OptionsSource>
270DefaultOptionsProvider::getRawOptions(llvm::StringRef FileName) {
271 std::vector<OptionsSource> Result;
272 Result.emplace_back(DefaultOptions, OptionsSourceTypeDefaultBinary);
273 return Result;
274}
275
276ConfigOptionsProvider::ConfigOptionsProvider(
277 ClangTidyGlobalOptions GlobalOptions, ClangTidyOptions DefaultOptions,
278 ClangTidyOptions ConfigOptions, ClangTidyOptions OverrideOptions,
279 llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> FS)
280 : FileOptionsBaseProvider(std::move(GlobalOptions),
281 std::move(DefaultOptions),
282 std::move(OverrideOptions), std::move(FS)),
283 ConfigOptions(std::move(ConfigOptions)) {}
284
285std::vector<OptionsSource>
286ConfigOptionsProvider::getRawOptions(llvm::StringRef FileName) {
287 std::vector<OptionsSource> RawOptions =
288 DefaultOptionsProvider::getRawOptions(FileName);
289 if (ConfigOptions.InheritParentConfig.value_or(false)) {
290 LLVM_DEBUG(llvm::dbgs()
291 << "Getting options for file " << FileName << "...\n");
292 assert(FS && "FS must be set.");
293
294 llvm::SmallString<128> AbsoluteFilePath(FileName);
295
296 if (!FS->makeAbsolute(AbsoluteFilePath)) {
297 addRawFileOptions(AbsoluteFilePath, RawOptions);
298 }
299 }
300 RawOptions.emplace_back(ConfigOptions,
301 OptionsSourceTypeConfigCommandLineOption);
302 RawOptions.emplace_back(OverrideOptions,
303 OptionsSourceTypeCheckCommandLineOption);
304 return RawOptions;
305}
306
307FileOptionsBaseProvider::FileOptionsBaseProvider(
308 ClangTidyGlobalOptions GlobalOptions, ClangTidyOptions DefaultOptions,
309 ClangTidyOptions OverrideOptions,
310 llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS)
311 : DefaultOptionsProvider(std::move(GlobalOptions),
312 std::move(DefaultOptions)),
313 OverrideOptions(std::move(OverrideOptions)), FS(std::move(VFS)) {
314 if (!FS)
315 FS = llvm::vfs::getRealFileSystem();
316 ConfigHandlers.emplace_back(".clang-tidy", parseConfiguration);
317}
318
319FileOptionsBaseProvider::FileOptionsBaseProvider(
320 ClangTidyGlobalOptions GlobalOptions, ClangTidyOptions DefaultOptions,
321 ClangTidyOptions OverrideOptions,
322 FileOptionsBaseProvider::ConfigFileHandlers ConfigHandlers)
323 : DefaultOptionsProvider(std::move(GlobalOptions),
324 std::move(DefaultOptions)),
325 OverrideOptions(std::move(OverrideOptions)),
326 ConfigHandlers(std::move(ConfigHandlers)) {}
327
328void FileOptionsBaseProvider::addRawFileOptions(
329 llvm::StringRef AbsolutePath, std::vector<OptionsSource> &CurOptions) {
330 auto CurSize = CurOptions.size();
331
332 // Look for a suitable configuration file in all parent directories of the
333 // file. Start with the immediate parent directory and move up.
334 StringRef Path = llvm::sys::path::parent_path(AbsolutePath);
335 for (StringRef CurrentPath = Path; !CurrentPath.empty();
336 CurrentPath = llvm::sys::path::parent_path(CurrentPath)) {
337 std::optional<OptionsSource> Result;
338
339 auto Iter = CachedOptions.find(CurrentPath);
340 if (Iter != CachedOptions.end())
341 Result = Iter->second;
342
343 if (!Result)
344 Result = tryReadConfigFile(CurrentPath);
345
346 if (Result) {
347 // Store cached value for all intermediate directories.
348 while (Path != CurrentPath) {
349 LLVM_DEBUG(llvm::dbgs()
350 << "Caching configuration for path " << Path << ".\n");
351 if (!CachedOptions.count(Path))
352 CachedOptions[Path] = *Result;
353 Path = llvm::sys::path::parent_path(Path);
354 }
355 CachedOptions[Path] = *Result;
356
357 CurOptions.push_back(*Result);
358 if (!Result->first.InheritParentConfig.value_or(false))
359 break;
360 }
361 }
362 // Reverse order of file configs because closer configs should have higher
363 // priority.
364 std::reverse(CurOptions.begin() + CurSize, CurOptions.end());
365}
366
367FileOptionsProvider::FileOptionsProvider(
368 ClangTidyGlobalOptions GlobalOptions, ClangTidyOptions DefaultOptions,
369 ClangTidyOptions OverrideOptions,
370 llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS)
371 : FileOptionsBaseProvider(std::move(GlobalOptions),
372 std::move(DefaultOptions),
373 std::move(OverrideOptions), std::move(VFS)) {}
374
375FileOptionsProvider::FileOptionsProvider(
376 ClangTidyGlobalOptions GlobalOptions, ClangTidyOptions DefaultOptions,
377 ClangTidyOptions OverrideOptions,
378 FileOptionsBaseProvider::ConfigFileHandlers ConfigHandlers)
379 : FileOptionsBaseProvider(
380 std::move(GlobalOptions), std::move(DefaultOptions),
381 std::move(OverrideOptions), std::move(ConfigHandlers)) {}
382
383// FIXME: This method has some common logic with clang::format::getStyle().
384// Consider pulling out common bits to a findParentFileWithName function or
385// similar.
386std::vector<OptionsSource>
387FileOptionsProvider::getRawOptions(StringRef FileName) {
388 LLVM_DEBUG(llvm::dbgs() << "Getting options for file " << FileName
389 << "...\n");
390 assert(FS && "FS must be set.");
391
392 llvm::SmallString<128> AbsoluteFilePath(FileName);
393
394 if (FS->makeAbsolute(AbsoluteFilePath))
395 return {};
396
397 std::vector<OptionsSource> RawOptions =
398 DefaultOptionsProvider::getRawOptions(AbsoluteFilePath.str());
399 addRawFileOptions(AbsoluteFilePath, RawOptions);
400 OptionsSource CommandLineOptions(OverrideOptions,
401 OptionsSourceTypeCheckCommandLineOption);
402
403 RawOptions.push_back(CommandLineOptions);
404 return RawOptions;
405}
406
407std::optional<OptionsSource>
408FileOptionsBaseProvider::tryReadConfigFile(StringRef Directory) {
409 assert(!Directory.empty());
410
411 llvm::ErrorOr<llvm::vfs::Status> DirectoryStatus = FS->status(Directory);
412
413 if (!DirectoryStatus || !DirectoryStatus->isDirectory()) {
414 llvm::errs() << "Error reading configuration from " << Directory
415 << ": directory doesn't exist.\n";
416 return std::nullopt;
417 }
418
419 for (const ConfigFileHandler &ConfigHandler : ConfigHandlers) {
420 SmallString<128> ConfigFile(Directory);
421 llvm::sys::path::append(ConfigFile, ConfigHandler.first);
422 LLVM_DEBUG(llvm::dbgs() << "Trying " << ConfigFile << "...\n");
423
424 llvm::ErrorOr<llvm::vfs::Status> FileStatus = FS->status(ConfigFile);
425
426 if (!FileStatus || !FileStatus->isRegularFile())
427 continue;
428
429 llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> Text =
430 FS->getBufferForFile(ConfigFile);
431 if (std::error_code EC = Text.getError()) {
432 llvm::errs() << "Can't read " << ConfigFile << ": " << EC.message()
433 << "\n";
434 continue;
435 }
436
437 // Skip empty files, e.g. files opened for writing via shell output
438 // redirection.
439 if ((*Text)->getBuffer().empty())
440 continue;
441 llvm::ErrorOr<ClangTidyOptions> ParsedOptions =
442 ConfigHandler.second({(*Text)->getBuffer(), ConfigFile});
443 if (!ParsedOptions) {
444 if (ParsedOptions.getError())
445 llvm::errs() << "Error parsing " << ConfigFile << ": "
446 << ParsedOptions.getError().message() << "\n";
447 continue;
448 }
449 return OptionsSource(*ParsedOptions, std::string(ConfigFile));
450 }
451 return std::nullopt;
452}
453
454/// Parses -line-filter option and stores it to the \c Options.
455std::error_code parseLineFilter(StringRef LineFilter,
456 clang::tidy::ClangTidyGlobalOptions &Options) {
457 llvm::yaml::Input Input(LineFilter);
458 Input >> Options.LineFilter;
459 return Input.error();
460}
461
462llvm::ErrorOr<ClangTidyOptions>
463parseConfiguration(llvm::MemoryBufferRef Config) {
464 llvm::yaml::Input Input(Config);
465 ClangTidyOptions Options;
466 Input >> Options;
467 if (Input.error())
468 return Input.error();
469 return Options;
470}
471
472static void diagHandlerImpl(const llvm::SMDiagnostic &Diag, void *Ctx) {
473 (*reinterpret_cast<DiagCallback *>(Ctx))(Diag);
474}
475
476llvm::ErrorOr<ClangTidyOptions>
477parseConfigurationWithDiags(llvm::MemoryBufferRef Config,
478 DiagCallback Handler) {
479 llvm::yaml::Input Input(Config, nullptr, Handler ? diagHandlerImpl : nullptr,
480 &Handler);
481 ClangTidyOptions Options;
482 Input >> Options;
483 if (Input.error())
484 return Input.error();
485 return Options;
486}
487
488std::string configurationAsText(const ClangTidyOptions &Options) {
489 std::string Text;
490 llvm::raw_string_ostream Stream(Text);
491 llvm::yaml::Output Output(Stream);
492 // We use the same mapping method for input and output, so we need a non-const
493 // reference here.
494 ClangTidyOptions NonConstValue = Options;
495 Output << NonConstValue;
496 return Stream.str();
497}
498
499} // namespace clang::tidy
500