Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions lib/ffi/clang/cursor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,33 @@ def abstract?
Lib.is_abstract(@cursor) != 0
end

# Check if this class/struct is copyable: it has no inaccessible
# (deleted or private) copy constructor, and every base class is
# copyable (recursively). Returns true on cursors that don't denote
# a class/struct, since the question doesn't apply.
#
# A class with no explicit copy constructor is treated as copyable —
# the implicit one is generated. We only flag explicit `= delete` or
# private/protected access.
#
# @returns [Boolean] True if instances of this type can be copied.
def copyable?
return true unless [:cursor_class_decl, :cursor_struct].include?(self.kind)

copy_constructors = self.find_by_kind(false, :cursor_constructor).select(&:copy_constructor?)
copy_constructors.each do |constructor|
return false if constructor.deleted? || constructor.private?
end

self.find_by_kind(false, :cursor_cxx_base_specifier).each do |base|
base_decl = base.type.declaration
next if base_decl.kind == :cursor_no_decl_found
return false unless base_decl.copyable?
end

true
end

# Check if this is a scoped enum.
# @returns [Boolean] True if it's a scoped enum.
def enum_scoped?
Expand Down
11 changes: 11 additions & 0 deletions lib/ffi/clang/types/type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ def intrinsic_type
type.unqualified_type
end

# Check if this type's declaration (after reference stripping)
# has an accessible copy constructor and copyable bases.
# Returns true for non-class types (fundamentals, pointers,
# enums) and for types whose declaration is unavailable
# (:cursor_no_decl_found).
#
# @returns [Boolean] True if instances of this type can be copied.
def copyable?
self.non_reference_type.declaration.copyable?
end

# Get the type of a template argument at the given index.
# For template specializations (e.g., `std::vector<int>`), this returns the type of
# the template argument at the specified position.
Expand Down
49 changes: 49 additions & 0 deletions spec/ffi/clang/cursor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1463,6 +1463,55 @@
end
end

describe "#copyable?" do
let(:simple_struct) do
find_matching(cursor_types) do |child, parent|
child.kind == :cursor_struct and child.spelling == "SimpleStruct"
end
end

let(:deleted_copy) do
find_matching(cursor_types) do |child, parent|
child.kind == :cursor_struct and child.spelling == "DeletedCopy"
end
end

let(:private_copy) do
find_matching(cursor_types) do |child, parent|
child.kind == :cursor_class_decl and child.spelling == "PrivateCopy"
end
end

let(:inherits_deleted_copy) do
find_matching(cursor_types) do |child, parent|
child.kind == :cursor_struct and child.spelling == "InheritsDeletedCopy"
end
end

it "returns true for a struct with implicit copy constructor" do
expect(simple_struct.copyable?).to be true
end

it "returns false for a struct with deleted copy constructor" do
expect(deleted_copy.copyable?).to be false
end

it "returns false for a class with private copy constructor" do
expect(private_copy.copyable?).to be false
end

it "returns false transitively when a base class is not copyable" do
expect(inherits_deleted_copy.copyable?).to be false
end

it "returns true on a non-class cursor" do
func = find_matching(cursor_types) do |child, parent|
child.kind == :cursor_function and child.spelling == "binary_operator_func"
end
expect(func.copyable?).to be true
end
end

describe "#copy_assignment_operator?" do
let(:copy_assign) do
find_matching(cursor_types) do |child, parent|
Expand Down
13 changes: 13 additions & 0 deletions spec/ffi/clang/fixtures/types.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ struct DeletedCopy {
DeletedCopy& operator=(DeletedCopy&&) = default;
};

// Private copy constructor — not copyable.
class PrivateCopy {
private:
PrivateCopy(const PrivateCopy&);
public:
PrivateCopy();
};

// Inherits from a non-copyable base — not transitively copyable.
struct InheritsDeletedCopy : public DeletedCopy {
int value;
};

// Explicit constructor
struct ExplicitCtor {
explicit ExplicitCtor(int x);
Expand Down
35 changes: 35 additions & 0 deletions spec/ffi/clang/type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,41 @@
end
end

describe "#copyable?" do
let(:cursor_types) {Index.new.parse_translation_unit(fixture_path("types.cxx")).cursor}

it "returns true for a copyable struct type" do
type = find_matching(cursor_types) do |child, parent|
child.kind == :cursor_struct and child.spelling == "SimpleStruct"
end.type
expect(type.copyable?).to be true
end

it "returns false for a struct type with deleted copy constructor" do
type = find_matching(cursor_types) do |child, parent|
child.kind == :cursor_struct and child.spelling == "DeletedCopy"
end.type
expect(type.copyable?).to be false
end

it "strips a reference before consulting the declaration" do
# A reference to DeletedCopy should be reported as not copyable —
# copyable? operates on the underlying type, not the reference.
ref_type = find_matching(cursor_cxx) do |child, parent|
child.kind == :cursor_cxx_method and child.spelling == "takesARef"
end.type.arg_types.to_a[0]
expect(ref_type.kind).to eq(:type_lvalue_ref)
expect(ref_type.copyable?).to be true # int& → int → copyable
end

it "returns true for a fundamental type" do
int_type = find_matching(cursor_cxx) do |child, parent|
child.kind == :cursor_field_decl and child.spelling == "int_member_a"
end.type
expect(int_type.copyable?).to be true
end
end

describe "#const_qualified?" do
let(:pointer_type) do
find_matching(cursor_cxx) do |child, parent|
Expand Down