qt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +27 -0
- data/README.md +303 -0
- data/Rakefile +94 -0
- data/examples/development_ordered_demos/01_dsl_hello.rb +22 -0
- data/examples/development_ordered_demos/02_live_layout_console.rb +137 -0
- data/examples/development_ordered_demos/03_component_showcase.rb +235 -0
- data/examples/development_ordered_demos/04_paint_simple.rb +147 -0
- data/examples/development_ordered_demos/05_tetris_simple.rb +295 -0
- data/examples/development_ordered_demos/06_timetrap_clockify.rb +759 -0
- data/examples/development_ordered_demos/07_peek_like_recorder.rb +597 -0
- data/examples/qtproject/widgets/itemviews/spreadsheet/main.rb +252 -0
- data/examples/qtproject/widgets/widgetsgallery/main.rb +184 -0
- data/ext/qt_ruby_bridge/extconf.rb +75 -0
- data/ext/qt_ruby_bridge/qt_ruby_runtime.hpp +23 -0
- data/ext/qt_ruby_bridge/runtime_events.cpp +408 -0
- data/ext/qt_ruby_bridge/runtime_signals.cpp +212 -0
- data/lib/qt/application_lifecycle.rb +44 -0
- data/lib/qt/bridge.rb +95 -0
- data/lib/qt/children_tracking.rb +15 -0
- data/lib/qt/constants.rb +10 -0
- data/lib/qt/date_time_codec.rb +104 -0
- data/lib/qt/errors.rb +6 -0
- data/lib/qt/event_runtime.rb +139 -0
- data/lib/qt/event_runtime_dispatch.rb +35 -0
- data/lib/qt/event_runtime_qobject_methods.rb +41 -0
- data/lib/qt/generated_constants_runtime.rb +33 -0
- data/lib/qt/inspectable.rb +29 -0
- data/lib/qt/key_sequence_codec.rb +22 -0
- data/lib/qt/native.rb +93 -0
- data/lib/qt/shortcut_compat.rb +30 -0
- data/lib/qt/string_codec.rb +44 -0
- data/lib/qt/variant_codec.rb +78 -0
- data/lib/qt/version.rb +5 -0
- data/lib/qt.rb +47 -0
- data/scripts/generate_bridge/ast_introspection.rb +267 -0
- data/scripts/generate_bridge/auto_method_spec_resolver.rb +37 -0
- data/scripts/generate_bridge/auto_methods.rb +438 -0
- data/scripts/generate_bridge/core_utils.rb +114 -0
- data/scripts/generate_bridge/cpp_method_return_emitter.rb +93 -0
- data/scripts/generate_bridge/ffi_api.rb +46 -0
- data/scripts/generate_bridge/free_function_specs.rb +289 -0
- data/scripts/generate_bridge/spec_discovery.rb +313 -0
- data/scripts/generate_bridge.rb +1113 -0
- metadata +99 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Qt
|
|
4
|
+
# Normalizes Ruby strings for bridge text paths.
|
|
5
|
+
module StringCodec
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
REPLACEMENT_CHAR = "\uFFFD"
|
|
9
|
+
|
|
10
|
+
def to_qt_text(value)
|
|
11
|
+
text = value.to_s
|
|
12
|
+
return normalize_binary_as_utf8(text) if text.encoding == Encoding::ASCII_8BIT
|
|
13
|
+
|
|
14
|
+
normalize_encoded_text(text)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def from_qt_text(value)
|
|
18
|
+
normalize_binary_as_utf8(value.to_s)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def binary_bytes?(value)
|
|
22
|
+
return false unless value.is_a?(String)
|
|
23
|
+
return false unless value.encoding == Encoding::ASCII_8BIT
|
|
24
|
+
|
|
25
|
+
!value.dup.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def normalize_encoded_text(text)
|
|
29
|
+
text.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: REPLACEMENT_CHAR)
|
|
30
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
|
31
|
+
normalize_binary_as_utf8(text.b)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def normalize_binary_as_utf8(text)
|
|
35
|
+
utf8 = text.dup.force_encoding(Encoding::UTF_8)
|
|
36
|
+
utf8.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: REPLACEMENT_CHAR)
|
|
37
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
|
38
|
+
text.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: REPLACEMENT_CHAR)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module_function :normalize_encoded_text, :normalize_binary_as_utf8
|
|
42
|
+
private_class_method :normalize_encoded_text, :normalize_binary_as_utf8
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Qt
|
|
6
|
+
# Encodes/decodes Ruby values for QVariant bridge transport.
|
|
7
|
+
module VariantCodec
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
PREFIX = 'qtv:'
|
|
11
|
+
DIRECT_ENCODERS = {
|
|
12
|
+
Integer => ->(v) { ['int', v.to_s] },
|
|
13
|
+
Float => ->(v) { ['float', v.to_s] },
|
|
14
|
+
String => lambda { |v|
|
|
15
|
+
if StringCodec.binary_bytes?(v)
|
|
16
|
+
['ba', base64_encode(v)]
|
|
17
|
+
else
|
|
18
|
+
['str', base64_encode(StringCodec.to_qt_text(v))]
|
|
19
|
+
end
|
|
20
|
+
}
|
|
21
|
+
}.freeze
|
|
22
|
+
JSON_ENCODABLE_CLASSES = [Array, Hash].freeze
|
|
23
|
+
|
|
24
|
+
def encode(value)
|
|
25
|
+
return "#{PREFIX}nil" if value.nil?
|
|
26
|
+
return encode_boolean(value) if [true, false].include?(value)
|
|
27
|
+
|
|
28
|
+
tag, payload = encoded_tag_and_payload(value)
|
|
29
|
+
"#{PREFIX}#{tag}:#{payload}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def decode(value)
|
|
33
|
+
raw = value.to_s
|
|
34
|
+
return raw unless raw.start_with?(PREFIX)
|
|
35
|
+
return nil if raw == "#{PREFIX}nil"
|
|
36
|
+
|
|
37
|
+
tag, payload = raw.delete_prefix(PREFIX).split(':', 2)
|
|
38
|
+
return raw if tag.nil? || payload.nil?
|
|
39
|
+
|
|
40
|
+
decode_typed_payload(tag, payload, raw)
|
|
41
|
+
rescue ArgumentError, JSON::ParserError
|
|
42
|
+
raw
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def encode_boolean(value)
|
|
46
|
+
"#{PREFIX}bool:#{value ? 1 : 0}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def decode_typed_payload(tag, payload, raw)
|
|
50
|
+
case tag
|
|
51
|
+
when 'bool' then payload == '1'
|
|
52
|
+
when 'int' then Integer(payload, 10)
|
|
53
|
+
when 'float' then Float(payload)
|
|
54
|
+
when 'str' then StringCodec.from_qt_text(base64_decode(payload))
|
|
55
|
+
when 'ba' then base64_decode(payload).b
|
|
56
|
+
when 'json' then JSON.parse(StringCodec.from_qt_text(base64_decode(payload)))
|
|
57
|
+
else raw
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def base64_encode(value)
|
|
62
|
+
[value].pack('m0')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def base64_decode(value)
|
|
66
|
+
value.unpack1('m0')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def encoded_tag_and_payload(value)
|
|
70
|
+
direct = DIRECT_ENCODERS[value.class]
|
|
71
|
+
return direct.call(value) if direct
|
|
72
|
+
|
|
73
|
+
return ['json', base64_encode(JSON.generate(value))] if JSON_ENCODABLE_CLASSES.any? { |k| value.is_a?(k) }
|
|
74
|
+
|
|
75
|
+
['str', base64_encode(value.to_s)]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/qt/version.rb
ADDED
data/lib/qt.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rbconfig'
|
|
4
|
+
|
|
5
|
+
ROOT = File.expand_path('..', __dir__)
|
|
6
|
+
GENERATOR = File.join(ROOT, 'scripts', 'generate_bridge.rb')
|
|
7
|
+
GENERATED_DIR = File.join(ROOT, 'build', 'generated')
|
|
8
|
+
GENERATED_WIDGETS = File.join(GENERATED_DIR, 'widgets.rb')
|
|
9
|
+
GENERATED_API = File.join(GENERATED_DIR, 'bridge_api.rb')
|
|
10
|
+
GENERATED_CONSTANTS = File.join(GENERATED_DIR, 'constants.rb')
|
|
11
|
+
|
|
12
|
+
unless File.exist?(GENERATED_WIDGETS) && File.exist?(GENERATED_API) && File.exist?(GENERATED_CONSTANTS)
|
|
13
|
+
ok = system(RbConfig.ruby, GENERATOR)
|
|
14
|
+
raise 'Failed to generate Qt Ruby bindings. Run: bundle exec rake compile' unless ok
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require_relative 'qt/version'
|
|
18
|
+
require_relative 'qt/errors'
|
|
19
|
+
require_relative 'qt/constants'
|
|
20
|
+
require_relative 'qt/string_codec'
|
|
21
|
+
require_relative 'qt/date_time_codec'
|
|
22
|
+
require_relative 'qt/key_sequence_codec'
|
|
23
|
+
require_relative 'qt/variant_codec'
|
|
24
|
+
require_relative 'qt/inspectable'
|
|
25
|
+
require_relative 'qt/children_tracking'
|
|
26
|
+
require_relative 'qt/application_lifecycle'
|
|
27
|
+
require_relative 'qt/bridge'
|
|
28
|
+
require_relative 'qt/native'
|
|
29
|
+
require_relative 'qt/event_runtime_dispatch'
|
|
30
|
+
require_relative 'qt/event_runtime_qobject_methods'
|
|
31
|
+
require_relative 'qt/event_runtime'
|
|
32
|
+
require GENERATED_WIDGETS
|
|
33
|
+
require_relative 'qt/shortcut_compat'
|
|
34
|
+
Qt::GeneratedConstantsRuntime.apply_generated_scoped_constants!(Qt)
|
|
35
|
+
|
|
36
|
+
# Root namespace for all Qt Ruby bindings.
|
|
37
|
+
module Qt
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Qt.constants(false)
|
|
41
|
+
.grep(/\AQ[A-Z]\w*\z/)
|
|
42
|
+
.sort
|
|
43
|
+
.each do |qt_const|
|
|
44
|
+
next if Object.const_defined?(qt_const, false)
|
|
45
|
+
|
|
46
|
+
Object.const_set(qt_const, Qt.const_get(qt_const))
|
|
47
|
+
end
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Collects enum/typedef names that should be cast as integers for FFI.
|
|
4
|
+
class IntCastTypeCollector
|
|
5
|
+
INTEGER_ALIAS_PATTERN = /\A(?:unsigned\s+|signed\s+)?(?:char|short|int|long|long long)\z/
|
|
6
|
+
|
|
7
|
+
def initialize(ast)
|
|
8
|
+
@ast = ast
|
|
9
|
+
@types = Set.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def collect
|
|
13
|
+
walk_ast_scoped(@ast) do |node, scope|
|
|
14
|
+
name = node['name']
|
|
15
|
+
next if name.nil? || name.empty?
|
|
16
|
+
|
|
17
|
+
qualified = (scope + [name]).join('::')
|
|
18
|
+
ast_append_int_cast_type!(@types, INTEGER_ALIAS_PATTERN, node, qualified)
|
|
19
|
+
end
|
|
20
|
+
@types
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Records method/constructor declarations and effective access for a class AST node.
|
|
25
|
+
class AstClassMemberRecorder
|
|
26
|
+
def initialize(class_name, methods_by_class, ctors_by_class, ctor_decls_by_class)
|
|
27
|
+
@class_name = class_name
|
|
28
|
+
@methods_by_class = methods_by_class
|
|
29
|
+
@ctors_by_class = ctors_by_class
|
|
30
|
+
@ctor_decls_by_class = ctor_decls_by_class
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def record(node)
|
|
34
|
+
current_access = node['tagUsed'] == 'struct' ? 'public' : 'private'
|
|
35
|
+
method_decl_count = ctor_decl_count = 0
|
|
36
|
+
Array(node['inner']).each do |inner|
|
|
37
|
+
current_access, method_decl_count, ctor_decl_count = record_inner(
|
|
38
|
+
inner, current_access, method_decl_count, ctor_decl_count
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
[method_decl_count, ctor_decl_count]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def record_inner(inner, current_access, method_decl_count, ctor_decl_count)
|
|
47
|
+
return [inner['access'] || current_access, method_decl_count, ctor_decl_count] if inner['kind'] == 'AccessSpecDecl'
|
|
48
|
+
return [current_access, method_decl_count + 1, ctor_decl_count] if record_method_member?(inner, current_access)
|
|
49
|
+
return [current_access, method_decl_count, ctor_decl_count + 1] if record_constructor_member?(inner, current_access)
|
|
50
|
+
|
|
51
|
+
[current_access, method_decl_count, ctor_decl_count]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def record_method_member?(inner, current_access)
|
|
55
|
+
return false unless inner['kind'] == 'CXXMethodDecl' && inner['name']
|
|
56
|
+
|
|
57
|
+
access = inner['access'] || current_access
|
|
58
|
+
@methods_by_class[@class_name][inner['name']] << inner.merge('__effective_access' => access)
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def record_constructor_member?(inner, current_access)
|
|
63
|
+
return false unless inner['kind'] == 'CXXConstructorDecl' && inner['name']
|
|
64
|
+
|
|
65
|
+
@ctors_by_class[@class_name] << inner['name']
|
|
66
|
+
@ctor_decls_by_class[@class_name] << inner.merge('__effective_access' => current_access)
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def pkg_config_cflags
|
|
72
|
+
cflags = `pkg-config --cflags Qt6Widgets 2>/dev/null`.strip
|
|
73
|
+
raise 'pkg-config Qt6Widgets is required' if cflags.empty?
|
|
74
|
+
|
|
75
|
+
cflags
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ast_dump
|
|
79
|
+
cflags = timed('pkg_config_cflags') { pkg_config_cflags }
|
|
80
|
+
|
|
81
|
+
Tempfile.create(['qt_ruby_probe', '.cpp']) do |file|
|
|
82
|
+
required_includes(GENERATOR_SCOPE).each { |inc| file.write("#include <#{inc}>\n") }
|
|
83
|
+
file.flush
|
|
84
|
+
|
|
85
|
+
cmd = "clang++ -std=c++17 -x c++ -Xclang -ast-dump=json -fsyntax-only #{cflags} #{file.path}"
|
|
86
|
+
out = timed('clang_ast_dump') { `#{cmd}` }
|
|
87
|
+
raise "clang AST dump failed: #{cmd}" unless Process.last_status.success?
|
|
88
|
+
|
|
89
|
+
timed('ast_json_parse') { JSON.parse(out, max_nesting: false) }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def walk_ast(node, &)
|
|
94
|
+
return unless node.is_a?(Hash)
|
|
95
|
+
|
|
96
|
+
yield node
|
|
97
|
+
Array(node['inner']).each { |child| walk_ast(child, &) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def walk_ast_scoped(node, scope = [], &)
|
|
101
|
+
return unless node.is_a?(Hash)
|
|
102
|
+
|
|
103
|
+
local_scope = scope
|
|
104
|
+
name = node['name']
|
|
105
|
+
local_scope = scope + [name] if name && !name.empty? && %w[NamespaceDecl CXXRecordDecl].include?(node['kind'])
|
|
106
|
+
|
|
107
|
+
yield node, local_scope
|
|
108
|
+
Array(node['inner']).each { |child| walk_ast_scoped(child, local_scope, &) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def ast_append_int_cast_type!(types, integer_alias_pattern, node, qualified)
|
|
112
|
+
case node['kind']
|
|
113
|
+
when 'EnumDecl'
|
|
114
|
+
types << qualified
|
|
115
|
+
when 'TypedefDecl', 'TypeAliasDecl'
|
|
116
|
+
aliased = node.dig('type', 'qualType').to_s.strip
|
|
117
|
+
return if aliased.empty?
|
|
118
|
+
|
|
119
|
+
types << qualified if aliased.match?(integer_alias_pattern)
|
|
120
|
+
types << qualified if aliased.include?('QFlags<')
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def ast_int_cast_type_set(ast)
|
|
125
|
+
@ast_int_cast_type_set_cache ||= {}.compare_by_identity
|
|
126
|
+
return @ast_int_cast_type_set_cache[ast] if @ast_int_cast_type_set_cache.key?(ast)
|
|
127
|
+
|
|
128
|
+
@ast_int_cast_type_set_cache[ast] = IntCastTypeCollector.new(ast).collect
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def collect_class_api(ast, class_name)
|
|
132
|
+
index = ast_class_index(ast)
|
|
133
|
+
methods = index[:methods_by_class].fetch(class_name, {}).keys
|
|
134
|
+
ctors = index[:ctors_by_class].fetch(class_name, [])
|
|
135
|
+
{ methods: methods, constructors: ctors }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def normalize_cpp_type_name(raw)
|
|
139
|
+
return nil if raw.nil? || raw.empty?
|
|
140
|
+
|
|
141
|
+
name = raw.dup
|
|
142
|
+
name = name.sub(/\A(class|struct)\s+/, '')
|
|
143
|
+
name = name.split('<').first
|
|
144
|
+
name = name.split(/\s+/).first
|
|
145
|
+
name = name.split('::').last
|
|
146
|
+
name&.strip
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def ast_record_base_classes(node, class_name, bases_by_class)
|
|
150
|
+
Array(node['bases']).each do |base|
|
|
151
|
+
type_info = base['type'] || {}
|
|
152
|
+
raw = type_info['desugaredQualType'] || type_info['qualType']
|
|
153
|
+
parsed_base = normalize_cpp_type_name(raw)
|
|
154
|
+
bases_by_class[class_name] << parsed_base if parsed_base && !parsed_base.empty?
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def ast_record_class_members(node, class_name, methods_by_class, ctors_by_class, ctor_decls_by_class)
|
|
159
|
+
AstClassMemberRecorder.new(
|
|
160
|
+
class_name, methods_by_class, ctors_by_class, ctor_decls_by_class
|
|
161
|
+
).record(node)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def ast_class_index_method_collections
|
|
165
|
+
{
|
|
166
|
+
methods_by_class: Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = [] } },
|
|
167
|
+
bases_by_class: Hash.new { |h, k| h[k] = [] }
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def ast_class_index_constructor_collections
|
|
172
|
+
{
|
|
173
|
+
ctors_by_class: Hash.new { |h, k| h[k] = [] },
|
|
174
|
+
ctor_decls_by_class: Hash.new { |h, k| h[k] = [] },
|
|
175
|
+
abstract_by_class: Hash.new(false)
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def ast_class_index_collections
|
|
180
|
+
ast_class_index_method_collections.merge(ast_class_index_constructor_collections)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def init_ast_class_index_data
|
|
184
|
+
ast_class_index_collections.merge(method_decl_count: 0, ctor_decl_count: 0)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def ast_index_track_record_decl(node, data)
|
|
188
|
+
class_name = node['name']
|
|
189
|
+
return if class_name.nil? || class_name.empty?
|
|
190
|
+
|
|
191
|
+
data[:abstract_by_class][class_name] ||= node.dig('definitionData', 'isAbstract') == true
|
|
192
|
+
ast_record_base_classes(node, class_name, data[:bases_by_class])
|
|
193
|
+
method_count, ctor_count = ast_record_class_members(
|
|
194
|
+
node, class_name, data[:methods_by_class], data[:ctors_by_class], data[:ctor_decls_by_class]
|
|
195
|
+
)
|
|
196
|
+
data[:method_decl_count] += method_count
|
|
197
|
+
data[:ctor_decl_count] += ctor_count
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def finalize_ast_class_index!(data)
|
|
201
|
+
data[:bases_by_class].each_value(&:uniq!)
|
|
202
|
+
data[:ctors_by_class].each_value(&:uniq!)
|
|
203
|
+
debug_log("ast_class_index classes=#{data[:methods_by_class].length} method_decls=#{data[:method_decl_count]}")
|
|
204
|
+
debug_log("ast_class_index ctor_decls=#{data[:ctor_decl_count]}")
|
|
205
|
+
data.slice(:methods_by_class, :bases_by_class, :ctors_by_class, :ctor_decls_by_class, :abstract_by_class)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def ast_class_index(ast)
|
|
209
|
+
@ast_class_index_cache ||= {}.compare_by_identity
|
|
210
|
+
return @ast_class_index_cache[ast] if @ast_class_index_cache.key?(ast)
|
|
211
|
+
|
|
212
|
+
data = init_ast_class_index_data
|
|
213
|
+
|
|
214
|
+
timed('ast_class_index_build') do
|
|
215
|
+
walk_ast(ast) do |node|
|
|
216
|
+
next unless node['kind'] == 'CXXRecordDecl'
|
|
217
|
+
|
|
218
|
+
ast_index_track_record_decl(node, data)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
@ast_class_index_cache[ast] = finalize_ast_class_index!(data)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def collect_method_decls(ast, class_name, method_name)
|
|
226
|
+
index = ast_class_index(ast)
|
|
227
|
+
index[:methods_by_class].dig(class_name, method_name) || []
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def collect_method_decls_with_bases(ast, class_name, method_name, visited = {})
|
|
231
|
+
return [] if class_name.nil? || class_name.empty? || visited[class_name]
|
|
232
|
+
|
|
233
|
+
visited[class_name] = true
|
|
234
|
+
own = collect_method_decls(ast, class_name, method_name)
|
|
235
|
+
return own unless own.empty?
|
|
236
|
+
|
|
237
|
+
collect_class_bases(ast, class_name).flat_map do |base|
|
|
238
|
+
collect_method_decls_with_bases(ast, base, method_name, visited)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def collect_class_bases(ast, class_name)
|
|
243
|
+
index = ast_class_index(ast)
|
|
244
|
+
Array(index[:bases_by_class][class_name]).uniq
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def collect_constructor_decls(ast, class_name)
|
|
248
|
+
index = ast_class_index(ast)
|
|
249
|
+
Array(index[:ctor_decls_by_class][class_name])
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def abstract_class?(ast, class_name)
|
|
253
|
+
index = ast_class_index(ast)
|
|
254
|
+
index[:abstract_by_class][class_name] == true
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def class_inherits?(ast, class_name, ancestor, visited = {})
|
|
258
|
+
return false if class_name.nil? || class_name.empty? || visited[class_name]
|
|
259
|
+
return true if class_name == ancestor
|
|
260
|
+
|
|
261
|
+
visited[class_name] = true
|
|
262
|
+
collect_class_bases(ast, class_name).any? { |base| class_inherits?(ast, base, ancestor, visited) }
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def class_has_method?(ast, class_name, method_name)
|
|
266
|
+
collect_class_api(ast, class_name)[:methods].include?(method_name)
|
|
267
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Resolves auto-generated methods for a class spec while tracking counters.
|
|
4
|
+
class AutoMethodSpecResolver
|
|
5
|
+
def initialize(spec:, auto_mode:, existing_names:, resolve_method:)
|
|
6
|
+
@spec = spec
|
|
7
|
+
@auto_mode = auto_mode
|
|
8
|
+
@existing_names = existing_names
|
|
9
|
+
@resolve_method = resolve_method
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def resolve(entry, skipped:, resolved:)
|
|
13
|
+
qt_name = entry_name(entry)
|
|
14
|
+
return [nil, skipped + 1, resolved] if existing_names.include?(qt_name)
|
|
15
|
+
|
|
16
|
+
method, skipped_entry = resolve_entry(entry)
|
|
17
|
+
return [nil, skipped + 1, resolved] if skipped_entry
|
|
18
|
+
|
|
19
|
+
[method, skipped, resolved + 1]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :spec, :auto_mode, :existing_names, :resolve_method
|
|
25
|
+
|
|
26
|
+
def entry_name(entry)
|
|
27
|
+
entry.is_a?(String) ? entry : entry[:qt_name]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def resolve_entry(entry)
|
|
31
|
+
method = resolve_method.call(entry)
|
|
32
|
+
return [method, false] unless method.nil?
|
|
33
|
+
return [nil, true] if auto_mode == :all
|
|
34
|
+
|
|
35
|
+
raise "Failed to auto-resolve #{spec[:qt_class]}##{entry_name(entry)}"
|
|
36
|
+
end
|
|
37
|
+
end
|