1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// A general interface for filtering and only acting on classes in Chromium C++
6// code.
7
8#include "ChromeClassTester.h"
9
10#include <algorithm>
11
12#include "clang/AST/AST.h"
13#include "clang/Basic/FileManager.h"
14#include "clang/Basic/SourceManager.h"
15
16#ifdef LLVM_ON_UNIX
17#include <sys/param.h>
18#endif
19#if defined(LLVM_ON_WIN32)
20#include <windows.h>
21#endif
22
23using namespace clang;
24using chrome_checker::Options;
25
26namespace {
27
28bool ends_with(const std::string& one, const std::string& two) {
29  if (two.size() > one.size())
30    return false;
31
32  return one.compare(one.size() - two.size(), two.size(), two) == 0;
33}
34
35}  // namespace
36
37ChromeClassTester::ChromeClassTester(CompilerInstance& instance,
38                                     const Options& options)
39    : options_(options),
40      instance_(instance),
41      diagnostic_(instance.getDiagnostics()) {
42  BuildBannedLists();
43}
44
45ChromeClassTester::~ChromeClassTester() {}
46
47void ChromeClassTester::CheckTag(TagDecl* tag) {
48  // We handle class types here where we have semantic information. We can only
49  // check structs/classes/enums here, but we get a bunch of nice semantic
50  // information instead of just parsing information.
51
52  if (CXXRecordDecl* record = dyn_cast<CXXRecordDecl>(tag)) {
53    if (InBannedNamespace(record))
54      return;
55
56    SourceLocation record_location = record->getInnerLocStart();
57    if (InBannedDirectory(record_location))
58      return;
59
60    // We sadly need to maintain a blacklist of types that violate these
61    // rules, but do so for good reason or due to limitations of this
62    // checker (i.e., we don't handle extern templates very well).
63    std::string base_name = record->getNameAsString();
64    if (IsIgnoredType(base_name))
65      return;
66
67    // We ignore all classes that end with "Matcher" because they're probably
68    // GMock artifacts.
69    if (ends_with(base_name, "Matcher"))
70        return;
71
72    CheckChromeClass(record_location, record);
73  } else if (EnumDecl* enum_decl = dyn_cast<EnumDecl>(tag)) {
74    SourceLocation enum_location = enum_decl->getInnerLocStart();
75    if (InBannedDirectory(enum_location))
76      return;
77
78    std::string base_name = enum_decl->getNameAsString();
79    if (IsIgnoredType(base_name))
80      return;
81
82    CheckChromeEnum(enum_location, enum_decl);
83  }
84}
85
86void ChromeClassTester::emitWarning(SourceLocation loc,
87                                    const char* raw_error) {
88  FullSourceLoc full(loc, instance().getSourceManager());
89  std::string err;
90  err = "[chromium-style] ";
91  err += raw_error;
92
93  DiagnosticIDs::Level level = getErrorLevel() == DiagnosticsEngine::Error
94      ? DiagnosticIDs::Error : DiagnosticIDs::Warning;
95
96  unsigned id = diagnostic().getDiagnosticIDs()->getCustomDiagID(level, err);
97  DiagnosticBuilder builder = diagnostic().Report(full, id);
98
99}
100
101bool ChromeClassTester::InBannedDirectory(SourceLocation loc) {
102  if (instance().getSourceManager().isInSystemHeader(loc))
103    return true;
104
105  std::string filename;
106  if (!GetFilename(loc, &filename)) {
107    // If the filename cannot be determined, simply treat this as a banned
108    // location, instead of going through the full lookup process.
109    return true;
110  }
111
112  // We need to special case scratch space; which is where clang does its
113  // macro expansion. We explicitly want to allow people to do otherwise bad
114  // things through macros that were defined due to third party libraries.
115  if (filename == "<scratch space>")
116    return true;
117
118  // Don't complain about autogenerated protobuf files.
119  if (ends_with(filename, ".pb.h")) {
120    return true;
121  }
122
123#if defined(LLVM_ON_UNIX)
124  // Resolve the symlinktastic relative path and make it absolute.
125  char resolvedPath[MAXPATHLEN];
126  if (options_.no_realpath) {
127    // Same reason as windows below, but we don't need to do
128    // the '\\' manipulation on linux.
129    filename.insert(filename.begin(), '/');
130  } else if (realpath(filename.c_str(), resolvedPath)) {
131    filename = resolvedPath;
132  }
133#endif
134
135#if defined(LLVM_ON_WIN32)
136  // Make path absolute.
137  if (options_.no_realpath) {
138    // This turns e.g. "gen/dir/file.cc" to "/gen/dir/file.cc" which lets the
139    // "/gen/" banned_dir work.
140    filename.insert(filename.begin(), '/');
141  } else {
142    // The Windows dance: Convert to UTF-16, call GetFullPathNameW, convert back
143    DWORD size_needed =
144        MultiByteToWideChar(CP_UTF8, 0, filename.data(), -1, nullptr, 0);
145    std::wstring utf16(size_needed, L'\0');
146    MultiByteToWideChar(CP_UTF8, 0, filename.data(), -1,
147                        &utf16[0], size_needed);
148
149    size_needed = GetFullPathNameW(utf16.data(), 0, nullptr, nullptr);
150    std::wstring full_utf16(size_needed, L'\0');
151    GetFullPathNameW(utf16.data(), full_utf16.size(), &full_utf16[0], nullptr);
152
153    size_needed = WideCharToMultiByte(CP_UTF8, 0, full_utf16.data(), -1,
154                                      nullptr, 0, nullptr, nullptr);
155    filename.resize(size_needed);
156    WideCharToMultiByte(CP_UTF8, 0, full_utf16.data(), -1, &filename[0],
157                        size_needed, nullptr, nullptr);
158  }
159
160  std::replace(filename.begin(), filename.end(), '\\', '/');
161#endif
162
163  for (const std::string& allowed_dir : allowed_directories_) {
164    // If any of the allowed directories occur as a component in filename,
165    // this file is allowed.
166    assert(allowed_dir.front() == '/' && "Allowed dir must start with '/'");
167    assert(allowed_dir.back() == '/' && "Allowed dir must end with '/'");
168
169    if (filename.find(allowed_dir) != std::string::npos)
170      return false;
171  }
172
173  for (const std::string& banned_dir : banned_directories_) {
174    // If any of the banned directories occur as a component in filename,
175    // this file is rejected.
176    assert(banned_dir.front() == '/' && "Banned dir must start with '/'");
177    assert(banned_dir.back() == '/' && "Banned dir must end with '/'");
178
179    if (filename.find(banned_dir) != std::string::npos)
180      return true;
181  }
182
183  return false;
184}
185
186bool ChromeClassTester::InBannedNamespace(const Decl* record) {
187  std::string n = GetNamespace(record);
188  if (!n.empty()) {
189    return std::find(banned_namespaces_.begin(), banned_namespaces_.end(), n)
190        != banned_namespaces_.end();
191  }
192
193  return false;
194}
195
196std::string ChromeClassTester::GetNamespace(const Decl* record) {
197  return GetNamespaceImpl(record->getDeclContext(), "");
198}
199
200bool ChromeClassTester::HasIgnoredBases(const CXXRecordDecl* record) {
201  for (const auto& base : record->bases()) {
202    CXXRecordDecl* base_record = base.getType()->getAsCXXRecordDecl();
203    if (!base_record)
204      continue;
205
206    const std::string& base_name = base_record->getQualifiedNameAsString();
207    if (ignored_base_classes_.count(base_name) > 0)
208      return true;
209    if (HasIgnoredBases(base_record))
210      return true;
211  }
212  return false;
213}
214
215bool ChromeClassTester::InImplementationFile(SourceLocation record_location) {
216  std::string filename;
217
218  // If |record_location| is a macro, check the whole chain of expansions.
219  const SourceManager& source_manager = instance_.getSourceManager();
220  while (true) {
221    if (GetFilename(record_location, &filename)) {
222      if (ends_with(filename, ".cc") || ends_with(filename, ".cpp") ||
223          ends_with(filename, ".mm")) {
224        return true;
225      }
226    }
227    if (!record_location.isMacroID()) {
228      break;
229    }
230    record_location =
231        source_manager.getImmediateExpansionRange(record_location).first;
232  }
233
234  return false;
235}
236
237void ChromeClassTester::BuildBannedLists() {
238  banned_namespaces_.emplace("std");
239  banned_namespaces_.emplace("__gnu_cxx");
240
241  if (options_.enforce_in_thirdparty_webkit) {
242    allowed_directories_.emplace("/third_party/WebKit/");
243  }
244
245  banned_directories_.emplace("/third_party/");
246  banned_directories_.emplace("/native_client/");
247  banned_directories_.emplace("/breakpad/");
248  banned_directories_.emplace("/courgette/");
249  banned_directories_.emplace("/ppapi/");
250  banned_directories_.emplace("/testing/");
251  banned_directories_.emplace("/v8/");
252  banned_directories_.emplace("/sdch/");
253  banned_directories_.emplace("/frameworks/");
254
255  // Don't check autogenerated headers.
256  // Make puts them below $(builddir_name)/.../gen and geni.
257  // Ninja puts them below OUTPUT_DIR/.../gen
258  // Xcode has a fixed output directory for everything.
259  banned_directories_.emplace("/gen/");
260  banned_directories_.emplace("/geni/");
261  banned_directories_.emplace("/xcodebuild/");
262
263  // Used in really low level threading code that probably shouldn't be out of
264  // lined.
265  ignored_record_names_.emplace("ThreadLocalBoolean");
266
267  // A complicated pickle derived struct that is all packed integers.
268  ignored_record_names_.emplace("Header");
269
270  // Part of the GPU system that uses multiple included header
271  // weirdness. Never getting this right.
272  ignored_record_names_.emplace("Validators");
273
274  // Has a UNIT_TEST only constructor. Isn't *terribly* complex...
275  ignored_record_names_.emplace("AutocompleteController");
276  ignored_record_names_.emplace("HistoryURLProvider");
277
278  // Used over in the net unittests. A large enough bundle of integers with 1
279  // non-pod class member. Probably harmless.
280  ignored_record_names_.emplace("MockTransaction");
281
282  // Enum type with _LAST members where _LAST doesn't mean last enum value.
283  ignored_record_names_.emplace("ServerFieldType");
284
285  // Used heavily in ui_base_unittests and once in views_unittests. Fixing this
286  // isn't worth the overhead of an additional library.
287  ignored_record_names_.emplace("TestAnimationDelegate");
288
289  // Part of our public interface that nacl and friends use. (Arguably, this
290  // should mean that this is a higher priority but fixing this looks hard.)
291  ignored_record_names_.emplace("PluginVersionInfo");
292
293  // Measured performance improvement on cc_perftests. See
294  // https://codereview.chromium.org/11299290/
295  ignored_record_names_.emplace("QuadF");
296
297  // Enum type with _LAST members where _LAST doesn't mean last enum value.
298  ignored_record_names_.emplace("ViewID");
299
300  // Ignore IPC::NoParams bases, since these structs are generated via
301  // macros and it makes it difficult to add explicit ctors.
302  ignored_base_classes_.emplace("IPC::NoParams");
303}
304
305std::string ChromeClassTester::GetNamespaceImpl(const DeclContext* context,
306                                                const std::string& candidate) {
307  switch (context->getDeclKind()) {
308    case Decl::TranslationUnit: {
309      return candidate;
310    }
311    case Decl::Namespace: {
312      const NamespaceDecl* decl = dyn_cast<NamespaceDecl>(context);
313      std::string name_str;
314      llvm::raw_string_ostream OS(name_str);
315      if (decl->isAnonymousNamespace())
316        OS << "<anonymous namespace>";
317      else
318        OS << *decl;
319      return GetNamespaceImpl(context->getParent(),
320                              OS.str());
321    }
322    default: {
323      return GetNamespaceImpl(context->getParent(), candidate);
324    }
325  }
326}
327
328bool ChromeClassTester::IsIgnoredType(const std::string& base_name) {
329  return ignored_record_names_.find(base_name) != ignored_record_names_.end();
330}
331
332bool ChromeClassTester::GetFilename(SourceLocation loc,
333                                    std::string* filename) {
334  const SourceManager& source_manager = instance_.getSourceManager();
335  SourceLocation spelling_location = source_manager.getSpellingLoc(loc);
336  PresumedLoc ploc = source_manager.getPresumedLoc(spelling_location);
337  if (ploc.isInvalid()) {
338    // If we're in an invalid location, we're looking at things that aren't
339    // actually stated in the source.
340    return false;
341  }
342
343  *filename = ploc.getFilename();
344  return true;
345}
346
347DiagnosticsEngine::Level ChromeClassTester::getErrorLevel() {
348  return diagnostic().getWarningsAsErrors() ? DiagnosticsEngine::Error
349                                            : DiagnosticsEngine::Warning;
350}
351