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