// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "BlinkGCPluginConsumer.h"
#include <algorithm>
#include <set>
#include "BadPatternFinder.h"
#include "CheckDispatchVisitor.h"
#include "CheckFieldsVisitor.h"
#include "CheckFinalizerVisitor.h"
#include "CheckForbiddenFieldsVisitor.h"
#include "CheckGCRootsVisitor.h"
#include "CheckTraceVisitor.h"
#include "CollectVisitor.h"
#include "JsonWriter.h"
#include "RecordInfo.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Sema/Sema.h"
#include "llvm/Support/TimeProfiler.h"
using namespace clang;
namespace {
// Use a local RAV implementation to simply collect all FunctionDecls marked for
// late template parsing. This happens with the flag -fdelayed-template-parsing,
// which is on by default in MSVC-compatible mode.
std::set<FunctionDecl*> GetLateParsedFunctionDecls(TranslationUnitDecl* decl) {
struct Visitor : public RecursiveASTVisitor<Visitor> {
bool VisitFunctionDecl(FunctionDecl* function_decl) {
if (function_decl->isLateTemplateParsed())
late_parsed_decls.insert(function_decl);
return true;
}
std::set<FunctionDecl*> late_parsed_decls;
} v;
v.TraverseDecl(decl);
return v.late_parsed_decls;
}
class EmptyStmtVisitor : public RecursiveASTVisitor<EmptyStmtVisitor> {
public:
static bool isEmpty(Stmt* stmt) {
EmptyStmtVisitor visitor;
visitor.TraverseStmt(stmt);
return visitor.empty_;
}
bool WalkUpFromCompoundStmt(CompoundStmt* stmt) {
empty_ = stmt->body_empty();
return false;
}
bool VisitStmt(Stmt*) {
empty_ = false;
return false;
}
private:
EmptyStmtVisitor() : empty_(true) {}
bool empty_;
};
const CXXRecordDecl* GetFirstTemplateArgAsCXXRecordDecl(
const CXXRecordDecl* gc_base) {
if (const auto* gc_base_template_id =
dyn_cast<ClassTemplateSpecializationDecl>(gc_base)) {
const TemplateArgumentList& gc_args =
gc_base_template_id->getTemplateArgs();
if (!gc_args.size() || gc_args[0].getKind() != TemplateArgument::Type)
return nullptr;
return gc_args[0].getAsType()->getAsCXXRecordDecl();
}
return nullptr;
}
} // namespace
BlinkGCPluginConsumer::BlinkGCPluginConsumer(
clang::CompilerInstance& instance,
const BlinkGCPluginOptions& options)
: instance_(instance),
reporter_(instance),
options_(options),
cache_(instance),
json_(0) {
// Only check structures in blink, cppgc and pdfium.
options_.checked_namespaces.insert("blink");
options_.checked_namespaces.insert("cppgc");
// Add Pdfium subfolders containing GCed classes.
options_.checked_directories.push_back("fpdfsdk/");
options_.checked_directories.push_back("fxjs/");
options_.checked_directories.push_back("xfa/");
// Ignore GC implementation files.
options_.ignored_directories.push_back(
"third_party/blink/renderer/platform/heap/collection_support/");
options_.ignored_directories.push_back("v8/src/heap/cppgc/");
options_.ignored_directories.push_back("v8/src/heap/cppgc-js/");
}
void BlinkGCPluginConsumer::HandleTranslationUnit(ASTContext& context) {
llvm::TimeTraceScope TimeScope(
"BlinkGCPluginConsumer::HandleTranslationUnit");
// Don't run the plugin if the compilation unit is already invalid.
if (reporter_.hasErrorOccurred())
return;
ParseFunctionTemplates(context.getTranslationUnitDecl());
CollectVisitor visitor;
visitor.TraverseDecl(context.getTranslationUnitDecl());
if (options_.dump_graph) {
std::error_code err;
SmallString<128> OutputFile(instance_.getFrontendOpts().OutputFile);
llvm::sys::path::replace_extension(OutputFile, "graph.json");
json_ = JsonWriter::from(instance_.createOutputFile(
OutputFile, // OutputPath
true, // Binary
true, // RemoveFileOnSignal
false, // UseTemporary
false)); // CreateMissingDirectories
if (!err && json_) {
json_->OpenList();
} else {
json_ = 0;
llvm::errs()
<< "[blink-gc] "
<< "Failed to create an output file for the object graph.\n";
}
}
for (const auto& record : visitor.record_decls())
CheckRecord(cache_.Lookup(record));
for (const auto& method : visitor.trace_decls())
CheckTracingMethod(method);
if (json_) {
json_->CloseList();
delete json_;
json_ = 0;
}
FindBadPatterns(context, reporter_, cache_, options_);
}
void BlinkGCPluginConsumer::ParseFunctionTemplates(TranslationUnitDecl* decl) {
if (!instance_.getLangOpts().DelayedTemplateParsing)
return; // Nothing to do.
std::set<FunctionDecl*> late_parsed_decls = GetLateParsedFunctionDecls(decl);
clang::Sema& sema = instance_.getSema();
for (const FunctionDecl* fd : late_parsed_decls) {
assert(fd->isLateTemplateParsed());
if (!Config::IsTraceMethod(fd))
continue;
if (instance_.getSourceManager().isInSystemHeader(
instance_.getSourceManager().getSpellingLoc(fd->getLocation())))
continue;
// Force parsing and AST building of the yet-uninstantiated function
// template trace method bodies.
clang::LateParsedTemplate* lpt = sema.LateParsedTemplateMap[fd].get();
sema.LateTemplateParser(sema.OpaqueParser, *lpt);
}
}
void BlinkGCPluginConsumer::CheckRecord(RecordInfo* info) {
if (IsIgnored(info))
return;
CXXRecordDecl* record = info->record();
// TODO: what should we do to check unions?
if (record->isUnion())
return;
// If this is the primary template declaration, check its specializations.
if (record->isThisDeclarationADefinition() &&
record->getDescribedClassTemplate()) {
ClassTemplateDecl* tmpl = record->getDescribedClassTemplate();
for (ClassTemplateDecl::spec_iterator it = tmpl->spec_begin();
it != tmpl->spec_end();
++it) {
CheckClass(cache_.Lookup(*it));
}
return;
}
CheckClass(info);
}
void BlinkGCPluginConsumer::CheckClass(RecordInfo* info) {
if (!info)
return;
if (CXXMethodDecl* trace = info->GetTraceMethod()) {
if (info->IsStackAllocated())
reporter_.TraceMethodForStackAllocatedClass(info, trace);
if (trace->isPureVirtual())
reporter_.ClassDeclaresPureVirtualTrace(info, trace);
} else if (info->RequiresTraceMethod()) {
reporter_.ClassRequiresTraceMethod(info);
}
// Check polymorphic classes that are GC-derived or have a trace method.
if (info->record()->hasDefinition() && info->record()->isPolymorphic()) {
// TODO: Check classes that inherit a trace method.
CXXMethodDecl* trace = info->GetTraceMethod();
if (trace || info->IsGCDerived())
CheckPolymorphicClass(info, trace);
}
{
CheckFieldsVisitor visitor(options_);
if (visitor.ContainsInvalidFields(info))
reporter_.ClassContainsInvalidFields(info, visitor.invalid_fields());
}
if (info->IsGCDerived()) {
// Check that CRTP pattern for GCed classes is correctly used.
if (auto* base_spec = info->GetDirectGCBase()) {
// Skip the check if base_spec name is dependent. The check will occur
// later for actual specializations.
if (!base_spec->getType()->isDependentType()) {
const CXXRecordDecl* base_decl =
base_spec->getType()->getAsCXXRecordDecl();
const CXXRecordDecl* first_arg =
GetFirstTemplateArgAsCXXRecordDecl(base_decl);
// The last check is for redeclaratation cases, for example, when
// explicit instantiation declaration is followed by the corresponding
// explicit instantiation definition.
if (!first_arg ||
first_arg->getFirstDecl() != info->record()->getFirstDecl()) {
reporter_.ClassMustCRTPItself(info, base_decl, base_spec);
}
}
}
// It is illegal for a class to be both stack allocated and garbage
// collected.
if (info->IsStackAllocated()) {
for (auto& base : info->GetBases()) {
RecordInfo* base_info = base.second.info();
if (Config::IsGCBase(base_info->name()) || base_info->IsGCDerived()) {
reporter_.StackAllocatedDerivesGarbageCollected(info, &base.second);
}
}
}
if (!info->IsGCMixin()) {
CheckLeftMostDerived(info);
CheckDispatch(info);
if (CXXMethodDecl* newop = info->DeclaresNewOperator()) {
if (!info->IsStackAllocated() &&
!Config::IsGCBase(newop->getParent()->getName()) &&
!Config::IsIgnoreAnnotated(newop)) {
reporter_.ClassOverridesNew(info, newop);
}
}
}
{
CheckGCRootsVisitor visitor(options_);
if (visitor.ContainsGCRoots(info))
reporter_.ClassContainsGCRoots(info, visitor.gc_roots());
reporter_.ClassContainsGCRootRefs(info, visitor.gc_root_refs());
}
CheckForbiddenFieldsVisitor visitor;
if (visitor.ContainsForbiddenFields(info)) {
reporter_.ClassContainsForbiddenFields(info, visitor.forbidden_fields());
}
if (info->NeedsFinalization())
CheckFinalization(info);
}
DumpClass(info);
}
CXXRecordDecl* BlinkGCPluginConsumer::GetDependentTemplatedDecl(
const Type& type) {
const TemplateSpecializationType* tmpl_type =
type.getAs<TemplateSpecializationType>();
if (!tmpl_type)
return 0;
TemplateDecl* tmpl_decl = tmpl_type->getTemplateName().getAsTemplateDecl();
if (!tmpl_decl)
return 0;
return dyn_cast<CXXRecordDecl>(tmpl_decl->getTemplatedDecl());
}
// The GC infrastructure assumes that if the vtable of a polymorphic
// base-class is not initialized for a given object (ie, it is partially
// initialized) then the object does not need to be traced. Thus, we must
// ensure that any polymorphic class with a trace method does not have any
// tractable fields that are initialized before we are sure that the vtable
// and the trace method are both defined. There are two cases that need to
// hold to satisfy that assumption:
//
// 1. If trace is virtual, then it must be defined in the left-most base.
// This ensures that if the vtable is initialized then it contains a pointer
// to the trace method.
//
// 2. If trace is non-virtual, then the trace method is defined and we must
// ensure that the left-most base defines a vtable. This ensures that the
// first thing to be initialized when constructing the object is the vtable
// itself.
void BlinkGCPluginConsumer::CheckPolymorphicClass(
RecordInfo* info,
CXXMethodDecl* trace) {
CXXRecordDecl* left_most = info->record();
CXXRecordDecl::base_class_iterator it = left_most->bases_begin();
CXXRecordDecl* left_most_base = 0;
while (it != left_most->bases_end()) {
left_most_base = it->getType()->getAsCXXRecordDecl();
if (!left_most_base && it->getType()->isDependentType())
left_most_base = RecordInfo::GetDependentTemplatedDecl(*it->getType());
// TODO: Find a way to correctly check actual instantiations
// for dependent types. The escape below will be hit, eg, when
// we have a primary template with no definition and
// specializations for each case (such as SupplementBase) in
// which case we don't succeed in checking the required
// properties.
if (!left_most_base || !left_most_base->hasDefinition())
return;
StringRef name = left_most_base->getName();
// We know GCMixin base defines virtual trace.
if (Config::IsGCMixinBase(name))
return;
// Stop with the left-most prior to a safe polymorphic base (a safe base
// is non-polymorphic and contains no fields).
if (Config::IsSafePolymorphicBase(name))
break;
left_most = left_most_base;
it = left_most->bases_begin();
}
if (RecordInfo* left_most_info = cache_.Lookup(left_most)) {
// Check condition (1):
if (trace && trace->isVirtual()) {
if (CXXMethodDecl* trace = left_most_info->GetTraceMethod()) {
if (trace->isVirtual())
return;
}
reporter_.BaseClassMustDeclareVirtualTrace(info, left_most);
return;
}
// Check condition (2):
if (DeclaresVirtualMethods(left_most))
return;
if (left_most_base) {
// Get the base next to the "safe polymorphic base"
if (it != left_most->bases_end())
++it;
if (it != left_most->bases_end()) {
if (CXXRecordDecl* next_base = it->getType()->getAsCXXRecordDecl()) {
if (CXXRecordDecl* next_left_most = GetLeftMostBase(next_base)) {
if (DeclaresVirtualMethods(next_left_most))
return;
reporter_.LeftMostBaseMustBePolymorphic(info, next_left_most);
return;
}
}
}
}
reporter_.LeftMostBaseMustBePolymorphic(info, left_most);
}
}
CXXRecordDecl* BlinkGCPluginConsumer::GetLeftMostBase(
CXXRecordDecl* left_most) {
CXXRecordDecl::base_class_iterator it = left_most->bases_begin();
while (it != left_most->bases_end()) {
if (it->getType()->isDependentType())
left_most = RecordInfo::GetDependentTemplatedDecl(*it->getType());
else
left_most = it->getType()->getAsCXXRecordDecl();
if (!left_most || !left_most->hasDefinition())
return 0;
it = left_most->bases_begin();
}
return left_most;
}
bool BlinkGCPluginConsumer::DeclaresVirtualMethods(CXXRecordDecl* decl) {
CXXRecordDecl::method_iterator it = decl->method_begin();
for (; it != decl->method_end(); ++it)
if (it->isVirtual() && !it->isPureVirtual())
return true;
return false;
}
void BlinkGCPluginConsumer::CheckLeftMostDerived(RecordInfo* info) {
CXXRecordDecl* left_most = GetLeftMostBase(info->record());
if (!left_most)
return;
if (!Config::IsGCBase(left_most->getName()) || Config::IsGCMixinBase(left_most->getName()))
reporter_.ClassMustLeftMostlyDeriveGC(info);
}
void BlinkGCPluginConsumer::CheckDispatch(RecordInfo* info) {
CXXMethodDecl* trace_dispatch = info->GetTraceDispatchMethod();
CXXMethodDecl* finalize_dispatch = info->GetFinalizeDispatchMethod();
if (!trace_dispatch && !finalize_dispatch)
return;
CXXRecordDecl* base = trace_dispatch ? trace_dispatch->getParent()
: finalize_dispatch->getParent();
// Check that dispatch methods are defined at the base.
if (base == info->record()) {
if (!trace_dispatch)
reporter_.MissingTraceDispatchMethod(info);
}
// Check that classes implementing manual dispatch do not have vtables.
if (info->record()->isPolymorphic()) {
reporter_.VirtualAndManualDispatch(
info, trace_dispatch ? trace_dispatch : finalize_dispatch);
}
// If this is a non-abstract class check that it is dispatched to.
// TODO: Create a global variant of this local check. We can only check if
// the dispatch body is known in this compilation unit.
if (info->IsConsideredAbstract())
return;
const FunctionDecl* defn;
if (trace_dispatch && trace_dispatch->isDefined(defn)) {
CheckDispatchVisitor visitor(info);
visitor.TraverseStmt(defn->getBody());
if (!visitor.dispatched_to_receiver())
reporter_.MissingTraceDispatch(defn, info);
}
if (finalize_dispatch && finalize_dispatch->isDefined(defn)) {
CheckDispatchVisitor visitor(info);
visitor.TraverseStmt(defn->getBody());
if (!visitor.dispatched_to_receiver())
reporter_.MissingFinalizeDispatch(defn, info);
}
}
// TODO: Should we collect destructors similar to trace methods?
void BlinkGCPluginConsumer::CheckFinalization(RecordInfo* info) {
CXXDestructorDecl* dtor = info->record()->getDestructor();
if (!dtor || !dtor->hasBody())
return;
CheckFinalizerVisitor visitor(&cache_);
visitor.TraverseCXXMethodDecl(dtor);
if (!visitor.finalized_fields().empty()) {
reporter_.FinalizerAccessesFinalizedFields(dtor,
visitor.finalized_fields());
}
}
void BlinkGCPluginConsumer::CheckTracingMethod(CXXMethodDecl* method) {
RecordInfo* parent = cache_.Lookup(method->getParent());
if (IsIgnored(parent))
return;
// Check templated tracing methods by checking the template instantiations.
// Specialized templates are handled as ordinary classes.
if (ClassTemplateDecl* tmpl =
parent->record()->getDescribedClassTemplate()) {
for (ClassTemplateDecl::spec_iterator it = tmpl->spec_begin();
it != tmpl->spec_end();
++it) {
// Check trace using each template instantiation as the holder.
if (Config::IsTemplateInstantiation(*it))
CheckTraceOrDispatchMethod(cache_.Lookup(*it), method);
}
return;
}
CheckTraceOrDispatchMethod(parent, method);
}
void BlinkGCPluginConsumer::CheckTraceOrDispatchMethod(
RecordInfo* parent,
CXXMethodDecl* method) {
Config::TraceMethodType trace_type = Config::GetTraceMethodType(method);
if (trace_type == Config::TRACE_AFTER_DISPATCH_METHOD ||
!parent->GetTraceDispatchMethod()) {
CheckTraceMethod(parent, method, trace_type);
}
// Dispatch methods are checked when we identify subclasses.
}
void BlinkGCPluginConsumer::CheckTraceMethod(
RecordInfo* parent,
CXXMethodDecl* trace,
Config::TraceMethodType trace_type) {
// A trace method must not override any non-virtual trace methods.
if (trace_type == Config::TRACE_METHOD) {
for (auto& base : parent->GetBases())
if (CXXMethodDecl* other = base.second.info()->InheritsNonVirtualTrace())
reporter_.OverriddenNonVirtualTrace(parent, trace, other);
}
CheckTraceVisitor visitor(trace, parent, &cache_);
visitor.TraverseCXXMethodDecl(trace);
for (auto& base : parent->GetBases())
if (!base.second.IsProperlyTraced())
reporter_.BaseRequiresTracing(parent, trace, base.first);
for (auto& field : parent->GetFields()) {
if (!field.second.IsProperlyTraced() ||
field.second.IsInproperlyTraced()) {
// Report one or more tracing-related field errors.
reporter_.FieldsImproperlyTraced(parent, trace);
break;
}
}
}
void BlinkGCPluginConsumer::DumpClass(RecordInfo* info) {
if (!json_)
return;
json_->OpenObject();
json_->Write("name", info->record()->getQualifiedNameAsString());
json_->Write("loc", GetLocString(info->record()->getBeginLoc()));
json_->CloseObject();
class DumpEdgeVisitor : public RecursiveEdgeVisitor {
public:
DumpEdgeVisitor(JsonWriter* json) : json_(json) {}
void DumpEdge(RecordInfo* src,
RecordInfo* dst,
const std::string& lbl,
const Edge::LivenessKind& kind,
const std::string& loc) {
json_->OpenObject();
json_->Write("src", src->record()->getQualifiedNameAsString());
json_->Write("dst", dst->record()->getQualifiedNameAsString());
json_->Write("lbl", lbl);
json_->Write("kind", kind);
json_->Write("loc", loc);
json_->Write("ptr",
!Parent() ? "val" :
Parent()->IsRawPtr() ?
(static_cast<RawPtr*>(Parent())->HasReferenceType() ?
"reference" : "raw") :
Parent()->IsRefPtr() ? "ref" :
Parent()->IsUniquePtr() ? "unique" :
(Parent()->IsMember() || Parent()->IsWeakMember()) ? "mem" :
"val");
json_->CloseObject();
}
void DumpField(RecordInfo* src, FieldPoint* point, const std::string& loc) {
src_ = src;
point_ = point;
loc_ = loc;
point_->edge()->Accept(this);
}
void AtValue(Value* e) override {
// The liveness kind of a path from the point to this value
// is given by the innermost place that is non-strong.
Edge::LivenessKind kind = Edge::kStrong;
for (Context::iterator it = context().begin(); it != context().end();
++it) {
Edge::LivenessKind pointer_kind = (*it)->Kind();
if (pointer_kind != Edge::kStrong) {
kind = pointer_kind;
break;
}
}
DumpEdge(
src_, e->value(), point_->field()->getNameAsString(), kind, loc_);
}
private:
JsonWriter* json_;
RecordInfo* src_;
FieldPoint* point_;
std::string loc_;
};
DumpEdgeVisitor visitor(json_);
for (auto& base : info->GetBases())
visitor.DumpEdge(info, base.second.info(), "<super>", Edge::kStrong,
GetLocString(base.second.spec().getBeginLoc()));
for (auto& field : info->GetFields())
visitor.DumpField(info, &field.second,
GetLocString(field.second.field()->getBeginLoc()));
}
std::string BlinkGCPluginConsumer::GetLocString(SourceLocation loc) {
const SourceManager& source_manager = instance_.getSourceManager();
PresumedLoc ploc = source_manager.getPresumedLoc(loc);
if (ploc.isInvalid())
return "";
std::string loc_str;
llvm::raw_string_ostream os(loc_str);
os << ploc.getFilename()
<< ":" << ploc.getLine()
<< ":" << ploc.getColumn();
return os.str();
}
bool BlinkGCPluginConsumer::IsIgnored(RecordInfo* record) {
return (!record || !InCheckedNamespaceOrDirectory(record) ||
IsIgnoredClass(record) || InIgnoredDirectory(record));
}
bool BlinkGCPluginConsumer::IsIgnoredClass(RecordInfo* info) {
// Ignore any class prefixed by SameSizeAs. These are used in
// Blink to verify class sizes and don't need checking.
const std::string SameSizeAs = "SameSizeAs";
if (info->name().compare(0, SameSizeAs.size(), SameSizeAs) == 0)
return true;
return (options_.ignored_classes.find(info->name()) !=
options_.ignored_classes.end());
}
bool BlinkGCPluginConsumer::InIgnoredDirectory(RecordInfo* info) {
std::string filename;
if (!GetFilename(info->record()->getBeginLoc(), &filename))
return false; // TODO: should we ignore non-existing file locations?
#if defined(_WIN32)
std::replace(filename.begin(), filename.end(), '\\', '/');
#endif
for (const auto& ignored_dir : options_.ignored_directories)
if (filename.find(ignored_dir) != std::string::npos) {
return true;
}
return false;
}
bool BlinkGCPluginConsumer::InCheckedNamespaceOrDirectory(RecordInfo* info) {
if (!info)
return false;
for (DeclContext* context = info->record()->getDeclContext();
!context->isTranslationUnit();
context = context->getParent()) {
if (NamespaceDecl* decl = dyn_cast<NamespaceDecl>(context)) {
if (decl->isAnonymousNamespace())
return true;
if (options_.checked_namespaces.find(decl->getNameAsString()) !=
options_.checked_namespaces.end()) {
return true;
}
}
}
std::string filename;
if (!GetFilename(info->record()->getBeginLoc(), &filename)) {
return false;
}
#if defined(_WIN32)
std::replace(filename.begin(), filename.end(), '\\', '/');
#endif
for (const auto& checked_dir : options_.checked_directories) {
if (filename.find(checked_dir) != std::string::npos) {
return true;
}
}
return false;
}
bool BlinkGCPluginConsumer::GetFilename(SourceLocation loc,
std::string* filename) {
const SourceManager& source_manager = instance_.getSourceManager();
SourceLocation spelling_location = source_manager.getSpellingLoc(loc);
PresumedLoc ploc = source_manager.getPresumedLoc(spelling_location);
if (ploc.isInvalid()) {
// If we're in an invalid location, we're looking at things that aren't
// actually stated in the source.
return false;
}
*filename = ploc.getFilename();
return true;
}