mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-09-12 20:42:21 +00:00
Lagom: Remove LibJSGCVerifier
This commit is contained in:
parent
4ae7bbda52
commit
6587c1f350
Notes:
sideshowbarker
2024-07-17 05:58:46 +09:00
Author: https://github.com/mattco98
Commit: 6587c1f350
Pull-request: https://github.com/SerenityOS/serenity/pull/24027
Reviewed-by: https://github.com/ADKaster ✅
Reviewed-by: https://github.com/nico
7 changed files with 0 additions and 661 deletions
3
Meta/Lagom/Tools/LibJSGCVerifier/.gitignore
vendored
3
Meta/Lagom/Tools/LibJSGCVerifier/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
||||||
build/
|
|
||||||
cmake-build-debug/
|
|
||||||
venv/
|
|
|
@ -1,25 +0,0 @@
|
||||||
cmake_minimum_required(VERSION 3.24)
|
|
||||||
|
|
||||||
project(LibJSGCVerifier C CXX)
|
|
||||||
|
|
||||||
set(LAGOM_BUILD OFF CACHE BOOL "Build without the serenity toolchain Clang")
|
|
||||||
|
|
||||||
if (LAGOM_BUILD)
|
|
||||||
find_package(Clang 17 CONFIG REQUIRED)
|
|
||||||
else()
|
|
||||||
find_package(Clang CONFIG REQUIRED HINTS "../../../../Toolchain/Local/clang")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_executable(LibJSGCVerifier src/main.cpp src/CellsHandler.cpp)
|
|
||||||
|
|
||||||
target_include_directories(LibJSGCVerifier SYSTEM PRIVATE ${CLANG_INCLUDE_DIRS} ${LLVM_INCLUDE_DIRS})
|
|
||||||
target_compile_features(LibJSGCVerifier PRIVATE cxx_std_20)
|
|
||||||
target_link_libraries(LibJSGCVerifier PRIVATE clangTooling)
|
|
||||||
|
|
||||||
target_compile_options(LibJSGCVerifier PRIVATE
|
|
||||||
-Wall
|
|
||||||
-Wextra
|
|
||||||
-Werror
|
|
||||||
-Wno-unused
|
|
||||||
-fno-rtti
|
|
||||||
)
|
|
|
@ -1,44 +0,0 @@
|
||||||
### LibJSGCVerifier
|
|
||||||
|
|
||||||
This is a simple Clang tool to validate certain behavior relating to LibJS's GC. It currently validates
|
|
||||||
two things:
|
|
||||||
|
|
||||||
- For all types wrapped by `GCPtr` or `NonnullGCPtr`, that the wrapped type inherits from `Cell`
|
|
||||||
- For all types not wrapped by `GCPtr` or `NonnullGCPtr`, that the wrapped type does not inherit from `Cell`
|
|
||||||
(otherwise it should be wrapped).
|
|
||||||
|
|
||||||
This tool currently support being built with the Serenity Clang toolchain or the lagom Clang toolchain (in which case it won't be able to verify Serenity-only applications)
|
|
||||||
|
|
||||||
#### Building & Running with the Serenity toolchain
|
|
||||||
First build Serenity with the Clang toolchain for x86_64:
|
|
||||||
```bash
|
|
||||||
./Meta/serenity.sh build x86_64 Clang
|
|
||||||
```
|
|
||||||
|
|
||||||
Then build the tool with:
|
|
||||||
```bash
|
|
||||||
cmake -GNinja -B build
|
|
||||||
cmake --build build
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run the tool with:
|
|
||||||
```bash
|
|
||||||
src/main.py -b <path to serenity>/Build
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Building & Running with the Lagom toolchain
|
|
||||||
First build the Serenity lagom applications with:
|
|
||||||
```bash
|
|
||||||
./Meta/serenity.sh build lagom
|
|
||||||
```
|
|
||||||
|
|
||||||
Then build the tool with:
|
|
||||||
```bash
|
|
||||||
cmake -GNinja -DLAGOM_BUILD=ON -B build
|
|
||||||
cmake --build build
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run the tool with:
|
|
||||||
```bash
|
|
||||||
src/main.py -l -b <path to serenity>/Build
|
|
||||||
```
|
|
|
@ -1,423 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Matthew Olsson <mattco@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "CellsHandler.h"
|
|
||||||
#include <clang/AST/DeclCXX.h>
|
|
||||||
#include <clang/AST/Type.h>
|
|
||||||
#include <clang/ASTMatchers/ASTMatchFinder.h>
|
|
||||||
#include <clang/ASTMatchers/ASTMatchers.h>
|
|
||||||
#include <clang/Basic/Diagnostic.h>
|
|
||||||
#include <clang/Basic/Specifiers.h>
|
|
||||||
#include <clang/Frontend/CompilerInstance.h>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <llvm/Support/Casting.h>
|
|
||||||
#include <llvm/Support/JSON.h>
|
|
||||||
#include <llvm/Support/raw_ostream.h>
|
|
||||||
#include <unordered_set>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
AST_MATCHER_P(clang::Decl, hasAnnotation, std::string, name)
|
|
||||||
{
|
|
||||||
(void)Builder;
|
|
||||||
(void)Finder;
|
|
||||||
for (auto const* attr : Node.attrs()) {
|
|
||||||
if (auto const* annotate_attr = llvm::dyn_cast<clang::AnnotateAttr>(attr)) {
|
|
||||||
if (annotate_attr->getAnnotation() == name)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
class SimpleCollectMatchesCallback : public clang::ast_matchers::MatchFinder::MatchCallback {
|
|
||||||
public:
|
|
||||||
explicit SimpleCollectMatchesCallback(std::string name)
|
|
||||||
: m_name(std::move(name))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void run(clang::ast_matchers::MatchFinder::MatchResult const& result) override
|
|
||||||
{
|
|
||||||
if (auto const* node = result.Nodes.getNodeAs<T>(m_name))
|
|
||||||
m_matches.push_back(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto const& matches() const { return m_matches; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::string m_name;
|
|
||||||
std::vector<T const*> m_matches;
|
|
||||||
};
|
|
||||||
|
|
||||||
CollectCellsHandler::CollectCellsHandler()
|
|
||||||
{
|
|
||||||
using namespace clang::ast_matchers;
|
|
||||||
|
|
||||||
m_finder.addMatcher(
|
|
||||||
traverse(
|
|
||||||
clang::TK_IgnoreUnlessSpelledInSource,
|
|
||||||
cxxRecordDecl(decl().bind("record-decl"))),
|
|
||||||
this);
|
|
||||||
|
|
||||||
auto non_capturable_var_decl = varDecl(
|
|
||||||
hasLocalStorage(),
|
|
||||||
unless(
|
|
||||||
anyOf(
|
|
||||||
// The declaration has an annotation:
|
|
||||||
// IGNORE_USE_IN_ESCAPING_LAMBDA Foo foo;
|
|
||||||
hasAnnotation("serenity::ignore_use_in_escaping_lambda"),
|
|
||||||
// The declaration is a reference:
|
|
||||||
// Foo& foo_ref = get_foo_ref();
|
|
||||||
// Foo* foo_ptr = get_foo_ptr();
|
|
||||||
// do_something([&foo_ref, &foo_ptr] {
|
|
||||||
// foo_ref.foo(); // Fine, foo_ref references the underlying Foo instance
|
|
||||||
// foo_ptr->foo(); // Bad, foo_ptr references the pointer on the stack above
|
|
||||||
// });
|
|
||||||
hasType(references(TypeMatcher(anything()))))));
|
|
||||||
|
|
||||||
auto bad_lambda_capture = lambdaCapture(anyOf(capturesThis(), capturesVar(non_capturable_var_decl))).bind("lambda-capture");
|
|
||||||
|
|
||||||
auto lambda_with_bad_capture = lambdaExpr(
|
|
||||||
anyOf(
|
|
||||||
// These are both required as they have slightly different behavior.
|
|
||||||
//
|
|
||||||
// We need forEachLambdaCapture because we need to go over every explicit capture in the capture list, as
|
|
||||||
// hasAnyCapture will just take the first capture in the list that matches the criteria (usually the `this`
|
|
||||||
// capture). Without it, if the first capture in the list was flagged as bad but is actually fine (e.g. the
|
|
||||||
// `this` capture, or a var capture by value), but there was a second capture in the list that was invalid,
|
|
||||||
// it would be skipped.
|
|
||||||
//
|
|
||||||
// But forEachLambdaCapture doesn't seem to find implicit captures, so we also need hasAnyCapture to handle
|
|
||||||
// captures that aren't explicitly listed in the capture list, but are still invalid.
|
|
||||||
forEachLambdaCapture(bad_lambda_capture),
|
|
||||||
hasAnyCapture(bad_lambda_capture)));
|
|
||||||
|
|
||||||
// Bind this varDecl so we can reference it later to make sure it isn't being called
|
|
||||||
auto lambda_with_bad_capture_decl = varDecl(hasInitializer(lambda_with_bad_capture)).bind("lambda");
|
|
||||||
|
|
||||||
m_finder.addMatcher(
|
|
||||||
traverse(
|
|
||||||
clang::TK_IgnoreUnlessSpelledInSource,
|
|
||||||
callExpr(
|
|
||||||
forEachArgumentWithParam(
|
|
||||||
anyOf(
|
|
||||||
// Match a lambda given directly in the function call
|
|
||||||
lambda_with_bad_capture,
|
|
||||||
// Matches an expression with a possibly-deeply-nested reference to a variable with a lambda type, e.g:
|
|
||||||
// auto lambda = [...] { ... };
|
|
||||||
// some_func(move(lambda));
|
|
||||||
has(declRefExpr(
|
|
||||||
to(lambda_with_bad_capture_decl),
|
|
||||||
// Avoid immediately invoked lambdas (i.e. match `move(lambda)` but not `move(lambda())`)
|
|
||||||
unless(hasParent(
|
|
||||||
// <lambda struct>::operator()(...)
|
|
||||||
cxxOperatorCallExpr(has(declRefExpr(to(equalsBoundNode("lambda")))))))))),
|
|
||||||
parmVarDecl(
|
|
||||||
allOf(
|
|
||||||
// It's important that the parameter has a RecordType, as a templated type can never escape its function
|
|
||||||
hasType(cxxRecordDecl()),
|
|
||||||
unless(hasAnnotation("serenity::noescape"))))
|
|
||||||
.bind("lambda-param-ref")))),
|
|
||||||
this);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool record_inherits_from_cell(clang::CXXRecordDecl const& record)
|
|
||||||
{
|
|
||||||
if (!record.isCompleteDefinition())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool inherits_from_cell = record.getQualifiedNameAsString() == "JS::Cell";
|
|
||||||
record.forallBases([&](clang::CXXRecordDecl const* base) -> bool {
|
|
||||||
if (base->getQualifiedNameAsString() == "JS::Cell") {
|
|
||||||
inherits_from_cell = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
return inherits_from_cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<clang::QualType> get_all_qualified_types(clang::QualType const& type)
|
|
||||||
{
|
|
||||||
std::vector<clang::QualType> qualified_types;
|
|
||||||
|
|
||||||
if (auto const* template_specialization = type->getAs<clang::TemplateSpecializationType>()) {
|
|
||||||
auto specialization_name = template_specialization->getTemplateName().getAsTemplateDecl()->getQualifiedNameAsString();
|
|
||||||
// Do not unwrap GCPtr/NonnullGCPtr/MarkedVector
|
|
||||||
if (specialization_name == "JS::GCPtr" || specialization_name == "JS::NonnullGCPtr" || specialization_name == "JS::RawGCPtr" || specialization_name == "JS::MarkedVector") {
|
|
||||||
qualified_types.push_back(type);
|
|
||||||
} else {
|
|
||||||
auto const template_arguments = template_specialization->template_arguments();
|
|
||||||
for (size_t i = 0; i < template_arguments.size(); i++) {
|
|
||||||
auto const& template_arg = template_arguments[i];
|
|
||||||
if (template_arg.getKind() == clang::TemplateArgument::Type) {
|
|
||||||
auto template_qualified_types = get_all_qualified_types(template_arg.getAsType());
|
|
||||||
std::move(template_qualified_types.begin(), template_qualified_types.end(), std::back_inserter(qualified_types));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
qualified_types.push_back(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
return qualified_types;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FieldValidationResult {
|
|
||||||
bool is_valid { false };
|
|
||||||
bool is_wrapped_in_gcptr { false };
|
|
||||||
bool needs_visiting { false };
|
|
||||||
};
|
|
||||||
|
|
||||||
FieldValidationResult validate_field(clang::FieldDecl const* field_decl)
|
|
||||||
{
|
|
||||||
auto type = field_decl->getType();
|
|
||||||
if (auto const* elaborated_type = llvm::dyn_cast<clang::ElaboratedType>(type.getTypePtr()))
|
|
||||||
type = elaborated_type->desugar();
|
|
||||||
|
|
||||||
FieldValidationResult result { .is_valid = true };
|
|
||||||
|
|
||||||
for (auto const& qualified_type : get_all_qualified_types(type)) {
|
|
||||||
if (auto const* pointer_decl = qualified_type->getAs<clang::PointerType>()) {
|
|
||||||
if (auto const* pointee = pointer_decl->getPointeeCXXRecordDecl()) {
|
|
||||||
if (record_inherits_from_cell(*pointee)) {
|
|
||||||
result.is_valid = false;
|
|
||||||
result.is_wrapped_in_gcptr = false;
|
|
||||||
result.needs_visiting = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (auto const* reference_decl = qualified_type->getAs<clang::ReferenceType>()) {
|
|
||||||
if (auto const* pointee = reference_decl->getPointeeCXXRecordDecl()) {
|
|
||||||
if (record_inherits_from_cell(*pointee)) {
|
|
||||||
result.is_valid = false;
|
|
||||||
result.is_wrapped_in_gcptr = false;
|
|
||||||
result.needs_visiting = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (auto const* specialization = qualified_type->getAs<clang::TemplateSpecializationType>()) {
|
|
||||||
auto template_type_name = specialization->getTemplateName().getAsTemplateDecl()->getName();
|
|
||||||
if (template_type_name != "GCPtr" && template_type_name != "NonnullGCPtr" && template_type_name != "RawGCPtr")
|
|
||||||
return result;
|
|
||||||
|
|
||||||
auto const template_args = specialization->template_arguments();
|
|
||||||
if (template_args.size() != 1)
|
|
||||||
return result; // Not really valid, but will produce a compilation error anyway
|
|
||||||
|
|
||||||
auto const& type_arg = template_args[0];
|
|
||||||
auto const* record_type = type_arg.getAsType()->getAs<clang::RecordType>();
|
|
||||||
if (!record_type)
|
|
||||||
return result;
|
|
||||||
|
|
||||||
auto const* record_decl = record_type->getAsCXXRecordDecl();
|
|
||||||
if (!record_decl->hasDefinition())
|
|
||||||
return result;
|
|
||||||
|
|
||||||
result.is_wrapped_in_gcptr = true;
|
|
||||||
result.is_valid = record_inherits_from_cell(*record_decl);
|
|
||||||
result.needs_visiting = template_type_name != "RawGCPtr";
|
|
||||||
} else if (auto const* record = qualified_type->getAsCXXRecordDecl()) {
|
|
||||||
if (record->getQualifiedNameAsString() == "JS::Value") {
|
|
||||||
result.needs_visiting = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void emit_record_json_data(clang::CXXRecordDecl const& record)
|
|
||||||
{
|
|
||||||
llvm::json::Object obj;
|
|
||||||
obj.insert({ "name", record.getQualifiedNameAsString() });
|
|
||||||
|
|
||||||
std::vector<std::string> bases;
|
|
||||||
record.forallBases([&](clang::CXXRecordDecl const* base) {
|
|
||||||
bases.push_back(base->getQualifiedNameAsString());
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
obj.insert({ "parents", bases });
|
|
||||||
|
|
||||||
bool has_cell_allocator = false;
|
|
||||||
bool has_js_constructor = false;
|
|
||||||
for (auto const& decl : record.decls()) {
|
|
||||||
if (auto* var_decl = llvm::dyn_cast<clang::VarDecl>(decl); var_decl && var_decl->getQualifiedNameAsString().ends_with("::cell_allocator")) {
|
|
||||||
has_cell_allocator = true;
|
|
||||||
} else if (auto* fn_decl = llvm::dyn_cast<clang::CXXMethodDecl>(decl); fn_decl && fn_decl->getQualifiedNameAsString().ends_with("::construct_impl")) {
|
|
||||||
has_js_constructor = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
obj.insert({ "has_cell_allocator", has_cell_allocator });
|
|
||||||
obj.insert({ "has_js_constructor", has_js_constructor });
|
|
||||||
|
|
||||||
llvm::outs() << std::move(obj) << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectCellsHandler::run(clang::ast_matchers::MatchFinder::MatchResult const& result)
|
|
||||||
{
|
|
||||||
check_cells(result);
|
|
||||||
check_lambda_captures(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectCellsHandler::check_cells(clang::ast_matchers::MatchFinder::MatchResult const& result)
|
|
||||||
{
|
|
||||||
using namespace clang::ast_matchers;
|
|
||||||
|
|
||||||
clang::CXXRecordDecl const* record = result.Nodes.getNodeAs<clang::CXXRecordDecl>("record-decl");
|
|
||||||
if (!record || !record->isCompleteDefinition() || (!record->isClass() && !record->isStruct()))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Cell triggers a bunch of warnings for its empty visit_edges implementation, but
|
|
||||||
// it doesn't have any members anyways so it's fine to just ignore.
|
|
||||||
auto qualified_name = record->getQualifiedNameAsString();
|
|
||||||
if (qualified_name == "JS::Cell")
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto& diag_engine = result.Context->getDiagnostics();
|
|
||||||
std::vector<clang::FieldDecl const*> fields_that_need_visiting;
|
|
||||||
|
|
||||||
for (clang::FieldDecl const* field : record->fields()) {
|
|
||||||
auto validation_results = validate_field(field);
|
|
||||||
if (!validation_results.is_valid) {
|
|
||||||
if (validation_results.is_wrapped_in_gcptr) {
|
|
||||||
auto diag_id = diag_engine.getCustomDiagID(clang::DiagnosticsEngine::Warning, "Specialization type must inherit from JS::Cell");
|
|
||||||
diag_engine.Report(field->getLocation(), diag_id);
|
|
||||||
} else {
|
|
||||||
auto diag_id = diag_engine.getCustomDiagID(clang::DiagnosticsEngine::Warning, "%0 to JS::Cell type should be wrapped in %1");
|
|
||||||
auto builder = diag_engine.Report(field->getLocation(), diag_id);
|
|
||||||
if (field->getType()->isReferenceType()) {
|
|
||||||
builder << "reference"
|
|
||||||
<< "JS::NonnullGCPtr";
|
|
||||||
} else {
|
|
||||||
builder << "pointer"
|
|
||||||
<< "JS::GCPtr";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (validation_results.needs_visiting) {
|
|
||||||
fields_that_need_visiting.push_back(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!record_inherits_from_cell(*record))
|
|
||||||
return;
|
|
||||||
|
|
||||||
emit_record_json_data(*record);
|
|
||||||
|
|
||||||
bool has_base = false;
|
|
||||||
for (auto const& decl : record->decls()) {
|
|
||||||
if (auto* alias_decl = llvm::dyn_cast<clang::TypeAliasDecl>(decl); alias_decl && alias_decl->getQualifiedNameAsString().ends_with("::Base")) {
|
|
||||||
has_base = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!has_base) {
|
|
||||||
auto diag_id = diag_engine.getCustomDiagID(clang::DiagnosticsEngine::Warning, "JS::Cell-inheriting class %0 is missing a JS_CELL() call in its header file");
|
|
||||||
auto builder = diag_engine.Report(record->getLocation(), diag_id);
|
|
||||||
builder << record->getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
clang::DeclarationName const name = &result.Context->Idents.get("visit_edges");
|
|
||||||
auto const* visit_edges_method = record->lookup(name).find_first<clang::CXXMethodDecl>();
|
|
||||||
if (!visit_edges_method && !fields_that_need_visiting.empty()) {
|
|
||||||
auto diag_id = diag_engine.getCustomDiagID(clang::DiagnosticsEngine::Warning, "JS::Cell-inheriting class %0 contains a GC-allocated member %1 but has no visit_edges method");
|
|
||||||
auto builder = diag_engine.Report(record->getLocation(), diag_id);
|
|
||||||
builder << record->getName()
|
|
||||||
<< fields_that_need_visiting[0];
|
|
||||||
}
|
|
||||||
if (!visit_edges_method || !visit_edges_method->getBody())
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Search for a call to Base::visit_edges. Note that this also has the nice side effect of
|
|
||||||
// ensuring the classes use JS_CELL/JS_OBJECT, as Base will not be defined if they do not.
|
|
||||||
|
|
||||||
MatchFinder base_visit_edges_finder;
|
|
||||||
SimpleCollectMatchesCallback<clang::MemberExpr> base_visit_edges_callback("member-call");
|
|
||||||
|
|
||||||
auto base_visit_edges_matcher = cxxMethodDecl(
|
|
||||||
ofClass(hasName(qualified_name)),
|
|
||||||
functionDecl(hasName("visit_edges")),
|
|
||||||
isOverride(),
|
|
||||||
hasDescendant(memberExpr(member(hasName("visit_edges"))).bind("member-call")));
|
|
||||||
|
|
||||||
base_visit_edges_finder.addMatcher(base_visit_edges_matcher, &base_visit_edges_callback);
|
|
||||||
base_visit_edges_finder.matchAST(*result.Context);
|
|
||||||
|
|
||||||
bool call_to_base_visit_edges_found = false;
|
|
||||||
|
|
||||||
for (auto const* call_expr : base_visit_edges_callback.matches()) {
|
|
||||||
// FIXME: Can we constrain the matcher above to avoid looking directly at the source code?
|
|
||||||
auto const* source_chars = result.SourceManager->getCharacterData(call_expr->getBeginLoc());
|
|
||||||
if (strncmp(source_chars, "Base::", 6) == 0) {
|
|
||||||
call_to_base_visit_edges_found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!call_to_base_visit_edges_found) {
|
|
||||||
auto diag_id = diag_engine.getCustomDiagID(clang::DiagnosticsEngine::Warning, "Missing call to Base::visit_edges");
|
|
||||||
diag_engine.Report(visit_edges_method->getBeginLoc(), diag_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for uses of all fields that need visiting. We don't ensure they are _actually_ visited
|
|
||||||
// with a call to visitor.visit(...), as that is too complex. Instead, we just assume that if the
|
|
||||||
// field is accessed at all, then it is visited.
|
|
||||||
|
|
||||||
if (fields_that_need_visiting.empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
MatchFinder field_access_finder;
|
|
||||||
SimpleCollectMatchesCallback<clang::MemberExpr> field_access_callback("member-expr");
|
|
||||||
|
|
||||||
auto field_access_matcher = memberExpr(
|
|
||||||
hasAncestor(cxxMethodDecl(hasName("visit_edges"))),
|
|
||||||
hasObjectExpression(hasType(pointsTo(cxxRecordDecl(hasName(record->getName()))))))
|
|
||||||
.bind("member-expr");
|
|
||||||
|
|
||||||
field_access_finder.addMatcher(field_access_matcher, &field_access_callback);
|
|
||||||
field_access_finder.matchAST(visit_edges_method->getASTContext());
|
|
||||||
|
|
||||||
std::unordered_set<std::string> fields_that_are_visited;
|
|
||||||
for (auto const* member_expr : field_access_callback.matches())
|
|
||||||
fields_that_are_visited.insert(member_expr->getMemberNameInfo().getAsString());
|
|
||||||
|
|
||||||
auto diag_id = diag_engine.getCustomDiagID(clang::DiagnosticsEngine::Warning, "GC-allocated member is not visited in %0::visit_edges");
|
|
||||||
|
|
||||||
for (auto const* field : fields_that_need_visiting) {
|
|
||||||
if (!fields_that_are_visited.contains(field->getNameAsString())) {
|
|
||||||
auto builder = diag_engine.Report(field->getBeginLoc(), diag_id);
|
|
||||||
builder << record->getName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectCellsHandler::check_lambda_captures(clang::ast_matchers::MatchFinder::MatchResult const& result)
|
|
||||||
{
|
|
||||||
auto& diag_engine = result.Context->getDiagnostics();
|
|
||||||
|
|
||||||
if (auto const* capture = result.Nodes.getNodeAs<clang::LambdaCapture>("lambda-capture")) {
|
|
||||||
if (capture->capturesThis() || capture->getCaptureKind() != clang::LCK_ByRef)
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto diag_id = diag_engine.getCustomDiagID(clang::DiagnosticsEngine::Warning, "Variable with local storage is captured by reference in a lambda that may be asynchronously executed");
|
|
||||||
diag_engine.Report(capture->getLocation(), diag_id);
|
|
||||||
|
|
||||||
clang::SourceLocation captured_var_location;
|
|
||||||
if (auto const* var_decl = llvm::dyn_cast<clang::VarDecl>(capture->getCapturedVar())) {
|
|
||||||
captured_var_location = var_decl->getTypeSourceInfo()->getTypeLoc().getBeginLoc();
|
|
||||||
} else {
|
|
||||||
captured_var_location = capture->getCapturedVar()->getLocation();
|
|
||||||
}
|
|
||||||
diag_id = diag_engine.getCustomDiagID(clang::DiagnosticsEngine::Note, "Annotate the variable declaration with IGNORE_USE_IN_ESCAPING_LAMBDA if it outlives the lambda");
|
|
||||||
diag_engine.Report(captured_var_location, diag_id);
|
|
||||||
|
|
||||||
auto const* param = result.Nodes.getNodeAs<clang::ParmVarDecl>("lambda-param-ref");
|
|
||||||
diag_id = diag_engine.getCustomDiagID(clang::DiagnosticsEngine::Note, "Annotate the parameter with NOESCAPE if the lambda will not outlive the function call");
|
|
||||||
diag_engine.Report(param->getTypeSourceInfo()->getTypeLoc().getBeginLoc(), diag_id);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Matthew Olsson <mattco@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <clang/ASTMatchers/ASTMatchFinder.h>
|
|
||||||
#include <clang/ASTMatchers/ASTMatchers.h>
|
|
||||||
#include <clang/Tooling/Tooling.h>
|
|
||||||
#include <unordered_set>
|
|
||||||
|
|
||||||
class CollectCellsHandler
|
|
||||||
: public clang::tooling::SourceFileCallbacks
|
|
||||||
, public clang::ast_matchers::MatchFinder::MatchCallback {
|
|
||||||
public:
|
|
||||||
CollectCellsHandler();
|
|
||||||
virtual ~CollectCellsHandler() override = default;
|
|
||||||
|
|
||||||
virtual void run(clang::ast_matchers::MatchFinder::MatchResult const& result) override;
|
|
||||||
|
|
||||||
clang::ast_matchers::MatchFinder& finder() { return m_finder; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
void check_cells(clang::ast_matchers::MatchFinder::MatchResult const& result);
|
|
||||||
void check_lambda_captures(clang::ast_matchers::MatchFinder::MatchResult const& result);
|
|
||||||
|
|
||||||
std::unordered_set<std::string> m_visited_classes;
|
|
||||||
clang::ast_matchers::MatchFinder m_finder;
|
|
||||||
};
|
|
|
@ -1,27 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Matthew Olsson <mattco@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "CellsHandler.h"
|
|
||||||
#include <clang/Tooling/CommonOptionsParser.h>
|
|
||||||
#include <clang/Tooling/Tooling.h>
|
|
||||||
#include <llvm/Support/CommandLine.h>
|
|
||||||
|
|
||||||
int main(int argc, char const** argv)
|
|
||||||
{
|
|
||||||
llvm::cl::OptionCategory s_tool_category("LibJSGCVerifier options");
|
|
||||||
auto maybe_parser = clang::tooling::CommonOptionsParser::create(argc, argv, s_tool_category);
|
|
||||||
if (!maybe_parser) {
|
|
||||||
llvm::errs() << maybe_parser.takeError();
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto& parser = maybe_parser.get();
|
|
||||||
clang::tooling::ClangTool tool(parser.getCompilations(), parser.getSourcePathList());
|
|
||||||
|
|
||||||
CollectCellsHandler collect_handler;
|
|
||||||
auto collect_action = clang::tooling::newFrontendActionFactory(&collect_handler.finder(), &collect_handler);
|
|
||||||
return tool.run(collect_action.get());
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import multiprocessing
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Relative to Userland directory
|
|
||||||
COMMON_PATHS_TO_SEARCH = [
|
|
||||||
'Libraries/LibJS',
|
|
||||||
'Libraries/LibMarkdown',
|
|
||||||
'Libraries/LibWeb',
|
|
||||||
'Services/WebContent',
|
|
||||||
'Services/WebWorker',
|
|
||||||
]
|
|
||||||
SERENITY_PATHS_TO_SEARCH = COMMON_PATHS_TO_SEARCH + [
|
|
||||||
'Applications/Assistant',
|
|
||||||
'Applications/Browser',
|
|
||||||
'Applications/Spreadsheet',
|
|
||||||
'Applications/TextEditor',
|
|
||||||
'DevTools/HackStudio',
|
|
||||||
]
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser('LibJSGCVerifier', description='A Clang tool to validate usage of the LibJS GC')
|
|
||||||
parser.add_argument('-b', '--build-path', required=True, help='Path to the project Build folder')
|
|
||||||
parser.add_argument('-l', '--lagom', action='store_true', required=False,
|
|
||||||
help='Use the lagom build instead of the serenity build')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
build_path = Path(args.build_path).resolve()
|
|
||||||
userland_path = build_path / '..' / 'Userland'
|
|
||||||
if args.lagom:
|
|
||||||
compile_commands_path = build_path / 'lagom' / 'compile_commands.json'
|
|
||||||
else:
|
|
||||||
compile_commands_path = build_path / 'x86_64clang' / 'compile_commands.json'
|
|
||||||
|
|
||||||
if not compile_commands_path.exists():
|
|
||||||
print(f'Could not find compile_commands.json in {compile_commands_path.parent}')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
paths = []
|
|
||||||
|
|
||||||
if args.lagom:
|
|
||||||
paths_to_search = COMMON_PATHS_TO_SEARCH
|
|
||||||
else:
|
|
||||||
paths_to_search = SERENITY_PATHS_TO_SEARCH
|
|
||||||
for containing_path in paths_to_search:
|
|
||||||
for root, dirs, files in os.walk(userland_path / containing_path):
|
|
||||||
for file in files:
|
|
||||||
if file.endswith('.cpp'):
|
|
||||||
paths.append(Path(root) / file)
|
|
||||||
|
|
||||||
|
|
||||||
def thread_init():
|
|
||||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
||||||
|
|
||||||
|
|
||||||
def thread_execute(file_path):
|
|
||||||
clang_args = [
|
|
||||||
'./build/LibJSGCVerifier',
|
|
||||||
'--extra-arg',
|
|
||||||
'-DUSING_AK_GLOBALLY=1', # To avoid errors about USING_AK_GLOBALLY not being defined at all
|
|
||||||
'--extra-arg',
|
|
||||||
'-DNULL=0', # To avoid errors about NULL not being defined at all
|
|
||||||
'-p',
|
|
||||||
compile_commands_path,
|
|
||||||
file_path
|
|
||||||
]
|
|
||||||
proc = subprocess.Popen(clang_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
stdout, stderr = proc.communicate()
|
|
||||||
print(f'Processed {file_path.resolve()}')
|
|
||||||
if stderr:
|
|
||||||
print(stderr.decode(), file=sys.stderr)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
if stdout:
|
|
||||||
for line in stdout.split(b'\n'):
|
|
||||||
if line:
|
|
||||||
results.append(json.loads(line))
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
with multiprocessing.Pool(processes=multiprocessing.cpu_count() - 2, initializer=thread_init) as pool:
|
|
||||||
try:
|
|
||||||
clang_results = []
|
|
||||||
for results in pool.map(thread_execute, paths):
|
|
||||||
clang_results.extend(results)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pool.terminate()
|
|
||||||
pool.join()
|
|
||||||
|
|
||||||
# Process output data
|
|
||||||
clang_results = {r['name']: r for r in clang_results}
|
|
||||||
leaf_objects = set(clang_results.keys())
|
|
||||||
for result in clang_results.values():
|
|
||||||
leaf_objects.difference_update(result['parents'])
|
|
||||||
|
|
||||||
for key, value in clang_results.items():
|
|
||||||
if key == 'JS::HeapBlock::FreelistEntry' or key == 'JS::HeapFunction':
|
|
||||||
# These are Heap-related classes and don't need their own allocator
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not value['has_cell_allocator'] and (key in leaf_objects or value['has_js_constructor']):
|
|
||||||
print(f'Class {key} is missing a JS_DECLARE_ALLOCATOR() declaration in its header file')
|
|
Loading…
Add table
Add a link
Reference in a new issue