ruby-bindgen 1.0.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.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE +25 -0
  4. data/README.md +68 -0
  5. data/Rakefile +8 -0
  6. data/bin/ruby-bindgen +133 -0
  7. data/docs/architecture.md +238 -0
  8. data/docs/c/c_bindings.md +65 -0
  9. data/docs/c/constants.md +21 -0
  10. data/docs/c/customizing.md +19 -0
  11. data/docs/c/filtering.md +24 -0
  12. data/docs/c/getting_started.md +56 -0
  13. data/docs/c/library_loading.md +53 -0
  14. data/docs/c/output.md +96 -0
  15. data/docs/c/types.md +61 -0
  16. data/docs/c/version_guards.md +105 -0
  17. data/docs/cmake/cmake_bindings.md +19 -0
  18. data/docs/cmake/filtering.md +26 -0
  19. data/docs/cmake/getting_started.md +52 -0
  20. data/docs/cmake/output.md +110 -0
  21. data/docs/configuration.md +351 -0
  22. data/docs/contributing.md +68 -0
  23. data/docs/cpp/buffers.md +24 -0
  24. data/docs/cpp/classes.md +139 -0
  25. data/docs/cpp/cpp_bindings.md +29 -0
  26. data/docs/cpp/customizing.md +124 -0
  27. data/docs/cpp/enums.md +42 -0
  28. data/docs/cpp/filtering.md +35 -0
  29. data/docs/cpp/getting_started.md +80 -0
  30. data/docs/cpp/iterators.md +94 -0
  31. data/docs/cpp/operators.md +170 -0
  32. data/docs/cpp/output.md +125 -0
  33. data/docs/cpp/templates.md +114 -0
  34. data/docs/examples.md +133 -0
  35. data/docs/index.md +133 -0
  36. data/docs/prior_art.md +37 -0
  37. data/docs/troubleshooting.md +243 -0
  38. data/docs/type_spelling.md +200 -0
  39. data/docs/updating_bindings.md +55 -0
  40. data/docs/version_guards.md +69 -0
  41. data/lib/ruby-bindgen/config.rb +63 -0
  42. data/lib/ruby-bindgen/generators/cmake/cmake.rb +171 -0
  43. data/lib/ruby-bindgen/generators/cmake/directory.erb +29 -0
  44. data/lib/ruby-bindgen/generators/cmake/guard.rb +55 -0
  45. data/lib/ruby-bindgen/generators/cmake/presets.erb +232 -0
  46. data/lib/ruby-bindgen/generators/cmake/project.erb +89 -0
  47. data/lib/ruby-bindgen/generators/ffi/callback.erb +1 -0
  48. data/lib/ruby-bindgen/generators/ffi/constant.erb +1 -0
  49. data/lib/ruby-bindgen/generators/ffi/enum_constant_decl.erb +1 -0
  50. data/lib/ruby-bindgen/generators/ffi/enum_decl.erb +4 -0
  51. data/lib/ruby-bindgen/generators/ffi/enum_decl_anonymous.erb +4 -0
  52. data/lib/ruby-bindgen/generators/ffi/ffi.rb +687 -0
  53. data/lib/ruby-bindgen/generators/ffi/field_decl.erb +1 -0
  54. data/lib/ruby-bindgen/generators/ffi/function.erb +5 -0
  55. data/lib/ruby-bindgen/generators/ffi/library.erb +39 -0
  56. data/lib/ruby-bindgen/generators/ffi/project.erb +18 -0
  57. data/lib/ruby-bindgen/generators/ffi/struct.erb +6 -0
  58. data/lib/ruby-bindgen/generators/ffi/translation_unit.erb +9 -0
  59. data/lib/ruby-bindgen/generators/ffi/typedef_decl.erb +1 -0
  60. data/lib/ruby-bindgen/generators/ffi/union.erb +6 -0
  61. data/lib/ruby-bindgen/generators/ffi/variable.erb +1 -0
  62. data/lib/ruby-bindgen/generators/ffi/version.erb +9 -0
  63. data/lib/ruby-bindgen/generators/ffi/version_method.erb +5 -0
  64. data/lib/ruby-bindgen/generators/generator.rb +52 -0
  65. data/lib/ruby-bindgen/generators/rice/auto_generated_base_class.erb +5 -0
  66. data/lib/ruby-bindgen/generators/rice/class.erb +31 -0
  67. data/lib/ruby-bindgen/generators/rice/class_template.erb +9 -0
  68. data/lib/ruby-bindgen/generators/rice/class_template_specialization.erb +10 -0
  69. data/lib/ruby-bindgen/generators/rice/constant.erb +9 -0
  70. data/lib/ruby-bindgen/generators/rice/constructor.erb +1 -0
  71. data/lib/ruby-bindgen/generators/rice/conversion_function.erb +4 -0
  72. data/lib/ruby-bindgen/generators/rice/cxx_iterator_method.erb +1 -0
  73. data/lib/ruby-bindgen/generators/rice/cxx_method.erb +6 -0
  74. data/lib/ruby-bindgen/generators/rice/enum_constant_decl.erb +7 -0
  75. data/lib/ruby-bindgen/generators/rice/enum_decl.erb +6 -0
  76. data/lib/ruby-bindgen/generators/rice/field_decl.erb +8 -0
  77. data/lib/ruby-bindgen/generators/rice/function.erb +6 -0
  78. data/lib/ruby-bindgen/generators/rice/function_pointer.rb +68 -0
  79. data/lib/ruby-bindgen/generators/rice/incomplete_class.erb +3 -0
  80. data/lib/ruby-bindgen/generators/rice/iterator_alias.erb +1 -0
  81. data/lib/ruby-bindgen/generators/rice/iterator_collector.rb +159 -0
  82. data/lib/ruby-bindgen/generators/rice/namespace.erb +5 -0
  83. data/lib/ruby-bindgen/generators/rice/non_member_operator_binary.erb +4 -0
  84. data/lib/ruby-bindgen/generators/rice/non_member_operator_inspect.erb +6 -0
  85. data/lib/ruby-bindgen/generators/rice/non_member_operator_unary.erb +4 -0
  86. data/lib/ruby-bindgen/generators/rice/operator[].erb +4 -0
  87. data/lib/ruby-bindgen/generators/rice/project.cpp.erb +18 -0
  88. data/lib/ruby-bindgen/generators/rice/project.hpp.erb +13 -0
  89. data/lib/ruby-bindgen/generators/rice/reference_qualifier.rb +495 -0
  90. data/lib/ruby-bindgen/generators/rice/rice.rb +1724 -0
  91. data/lib/ruby-bindgen/generators/rice/rice_include.hpp.erb +7 -0
  92. data/lib/ruby-bindgen/generators/rice/signature_builder.rb +230 -0
  93. data/lib/ruby-bindgen/generators/rice/template_resolver.rb +585 -0
  94. data/lib/ruby-bindgen/generators/rice/translation_unit.cpp.erb +40 -0
  95. data/lib/ruby-bindgen/generators/rice/translation_unit.hpp.erb +7 -0
  96. data/lib/ruby-bindgen/generators/rice/translation_unit.ipp.erb +3 -0
  97. data/lib/ruby-bindgen/generators/rice/type_index.rb +117 -0
  98. data/lib/ruby-bindgen/generators/rice/type_speller.rb +509 -0
  99. data/lib/ruby-bindgen/generators/rice/union.erb +5 -0
  100. data/lib/ruby-bindgen/generators/rice/variable.erb +5 -0
  101. data/lib/ruby-bindgen/inputter.rb +54 -0
  102. data/lib/ruby-bindgen/name_mapper.rb +65 -0
  103. data/lib/ruby-bindgen/namer.rb +138 -0
  104. data/lib/ruby-bindgen/outputter.rb +40 -0
  105. data/lib/ruby-bindgen/parser.rb +82 -0
  106. data/lib/ruby-bindgen/refinements/cursor.rb +57 -0
  107. data/lib/ruby-bindgen/refinements/string.rb +41 -0
  108. data/lib/ruby-bindgen/symbol_candidates.rb +282 -0
  109. data/lib/ruby-bindgen/symbol_entry.rb +21 -0
  110. data/lib/ruby-bindgen/symbols.rb +107 -0
  111. data/lib/ruby-bindgen/type_pointer_formatter.rb +35 -0
  112. data/lib/ruby-bindgen/version.rb +3 -0
  113. data/lib/ruby-bindgen.rb +19 -0
  114. data/ruby-bindgen.gemspec +52 -0
  115. metadata +260 -0
@@ -0,0 +1,495 @@
1
+ module RubyBindgen
2
+ module Generators
3
+ class Rice
4
+ # Qualifies source-written references using libclang spans for location and
5
+ # cursor metadata for the replacement text. This preserves the original
6
+ # written expression everywhere except the exact references that need
7
+ # namespace or class qualification once emitted outside their source scope.
8
+ class ReferenceQualifier
9
+ # Qualify names inside source-written text without trusting the source
10
+ # range text itself for replacement content.
11
+ #
12
+ # Examples:
13
+ # text = 'helper("helper")'
14
+ # => 'quoted::helper("helper")'
15
+ #
16
+ # text = 'Holder<Tag>::value'
17
+ # => 'QualifiedDefaults::Holder<QualifiedDefaults::Tag>::value'
18
+ def qualify_source_references(root_cursor, source_text, source_text_offset, qualify_decl_refs: true, substitutions: {})
19
+ range_replacements = []
20
+ type_fallbacks = []
21
+
22
+ root_cursor.find_by_kind(true, :cursor_type_ref, :cursor_template_ref) do |type_ref|
23
+ ref = type_ref.referenced
24
+ next unless ref && ref.kind != :cursor_invalid_file
25
+
26
+ simple_name = ref.spelling
27
+ if simple_name && substitutions.key?(simple_name)
28
+ range = type_ref.reference_name_range([:want_qualifier, :want_template_args, :want_single_piece])
29
+ replacement = source_range_replacement(source_text, source_text_offset, range, substitutions[simple_name])
30
+ if replacement
31
+ range_replacements << replacement.merge(kind: :type)
32
+ next
33
+ end
34
+ end
35
+
36
+ # Template type parameters stay visible by bare name in generated
37
+ # template code, so they should not be qualified here.
38
+ next if [:cursor_template_type_parameter, :cursor_template_template_parameter].include?(ref.kind)
39
+
40
+ begin
41
+ is_dependent_typedef = ref.kind == :cursor_typedef_decl && ref.semantic_parent.kind == :cursor_class_template
42
+ qualified_name = if is_dependent_typedef
43
+ "#{ref.semantic_parent.qualified_display_name}::#{ref.spelling}"
44
+ else
45
+ ref.qualified_name
46
+ end
47
+ simple_name = ref.spelling
48
+ next if simple_name.nil? || simple_name.empty?
49
+ next if simple_name == qualified_name
50
+
51
+ range = type_ref.reference_name_range([:want_qualifier, :want_template_args, :want_single_piece])
52
+ replacement = source_range_replacement(source_text, source_text_offset, range)
53
+ if replacement
54
+ start_index = replacement[:start_offset] - source_text_offset
55
+ end_index = replacement[:end_offset] - source_text_offset
56
+ start_index = expand_name_start_to_qualifier(source_text, start_index)
57
+ replacement = replacement.merge(start_offset: source_text_offset + start_index)
58
+ span_text = source_text.byteslice(start_index, end_index - start_index)
59
+ trailing_scope = source_text.byteslice(end_index, 2).to_s == '::'
60
+
61
+ replacement_name = if ref.kind == :cursor_class_template && trailing_scope && !span_text.include?('<')
62
+ ref.qualified_display_name
63
+ else
64
+ qualified_name
65
+ end
66
+ replacement_text = replacement_from_name_span(span_text, simple_name, replacement_name)
67
+ if is_dependent_typedef && replacement_text && !trailing_scope &&
68
+ !preceded_by_typename?(source_text, start_index)
69
+ replacement_text = "typename #{replacement_text}"
70
+ end
71
+
72
+ if replacement_text && replacement_text != span_text
73
+ range_replacements << replacement.merge(replacement: replacement_text, kind: :type)
74
+ next
75
+ end
76
+ end
77
+
78
+ type_fallbacks << [ref, simple_name, qualified_name, is_dependent_typedef]
79
+ rescue ArgumentError
80
+ # Skip if we can't get qualified name (e.g., invalid cursor)
81
+ end
82
+ end
83
+
84
+ decl_fallbacks = []
85
+ if qualify_decl_refs
86
+ decl_refs = root_cursor.find_by_kind(true, :cursor_decl_ref_expr).to_a
87
+ decl_refs = [root_cursor] + decl_refs if root_cursor.kind == :cursor_decl_ref_expr
88
+ decl_refs.each do |decl_ref|
89
+ ref = decl_ref.referenced
90
+ next unless ref && ref.kind != :cursor_invalid_file
91
+
92
+ begin
93
+ simple_name = ref.spelling
94
+ next if simple_name.nil? || simple_name.empty?
95
+
96
+ if substitutions.key?(simple_name)
97
+ range = if decl_ref.extent.text.include?('<')
98
+ decl_ref.spelling_name_range(0)
99
+ else
100
+ decl_ref.reference_name_range([:want_qualifier, :want_template_args, :want_single_piece])
101
+ end
102
+ replacement = source_range_replacement(source_text, source_text_offset, range, substitutions[simple_name])
103
+ if replacement
104
+ range_replacements << replacement.merge(kind: :decl)
105
+ next
106
+ end
107
+ end
108
+
109
+ next if ref.kind == :cursor_non_type_template_parameter
110
+
111
+ if ref.kind == :cursor_cxx_method
112
+ next if source_text.match?(/::#{Regexp.escape(simple_name)}\s*\(/)
113
+ end
114
+
115
+ qualified_name = if ref.kind == :cursor_enum_constant_decl &&
116
+ ref.semantic_parent.kind == :cursor_enum_decl &&
117
+ !ref.semantic_parent.enum_scoped?
118
+ enum_parent = ref.semantic_parent.semantic_parent
119
+ if enum_parent && enum_parent.kind == :cursor_namespace
120
+ "#{enum_parent.qualified_name}::#{simple_name}"
121
+ elsif ref.semantic_parent.anonymous? &&
122
+ enum_parent && enum_parent.kind != :cursor_translation_unit
123
+ "#{enum_parent.qualified_name}::#{simple_name}"
124
+ else
125
+ ref.qualified_name
126
+ end
127
+ elsif ref.semantic_parent.kind == :cursor_class_template
128
+ "#{ref.semantic_parent.qualified_display_name}::#{simple_name}"
129
+ else
130
+ ref.qualified_name
131
+ end
132
+
133
+ next if simple_name == qualified_name
134
+ next if qualified_name.start_with?('::')
135
+ next unless qualified_name.end_with?(simple_name)
136
+
137
+ range = if decl_ref.extent.text.include?('<')
138
+ decl_ref.spelling_name_range(0)
139
+ else
140
+ decl_ref.reference_name_range([:want_qualifier, :want_template_args, :want_single_piece])
141
+ end
142
+ replacement = source_range_replacement(source_text, source_text_offset, range)
143
+ if replacement
144
+ start_index = replacement[:start_offset] - source_text_offset
145
+ end_index = replacement[:end_offset] - source_text_offset
146
+ span_text = source_text.byteslice(start_index, end_index - start_index)
147
+ replacement_text = replacement_from_name_span(span_text, simple_name, qualified_name)
148
+ if replacement_text && replacement_text != span_text
149
+ range_replacements << replacement.merge(replacement: replacement_text, kind: :decl)
150
+ next
151
+ end
152
+ end
153
+
154
+ decl_fallbacks << [simple_name, qualified_name]
155
+ rescue ArgumentError
156
+ # Skip if we can't get qualified name
157
+ end
158
+ end
159
+
160
+ decl_replacements = range_replacements.select { |replacement| replacement[:kind] == :decl }
161
+ range_replacements.reject! do |replacement|
162
+ replacement[:kind] == :type &&
163
+ decl_replacements.any? do |decl_replacement|
164
+ decl_replacement[:start_offset] <= replacement[:start_offset] &&
165
+ decl_replacement[:end_offset] >= replacement[:end_offset]
166
+ end
167
+ end
168
+ end
169
+
170
+ range_replacements = collapse_same_start_replacements(range_replacements)
171
+
172
+ source_text = apply_source_replacements(source_text, source_text_offset, range_replacements) unless range_replacements.empty?
173
+
174
+ type_fallbacks.each do |type_fallback|
175
+ ref, simple_name, qualified_name, is_dependent_typedef = type_fallback
176
+ source_text = fallback_qualify_type_reference(source_text, ref, simple_name, qualified_name, is_dependent_typedef)
177
+ end
178
+
179
+ decl_fallbacks.each do |decl_fallback|
180
+ simple_name, qualified_name = decl_fallback
181
+ source_text = fallback_qualify_declaration_reference(source_text, simple_name, qualified_name)
182
+ end
183
+
184
+ source_text
185
+ end
186
+
187
+ # Expand the written name span to the desired fully qualified form
188
+ # without dropping template args or clobbering existing qualifiers.
189
+ #
190
+ # Examples:
191
+ # span_text = 'helper'
192
+ # simple_name = 'helper'
193
+ # qualified_name = 'quoted::helper'
194
+ # => 'quoted::helper'
195
+ #
196
+ # span_text = 'makePtr<inner::IndexParams>'
197
+ # simple_name = 'makePtr'
198
+ # qualified_name = 'outer::makePtr'
199
+ # => 'outer::makePtr<inner::IndexParams>'
200
+ #
201
+ # span_text = 'PerfLevel::SLOW'
202
+ # simple_name = 'SLOW'
203
+ # qualified_name = 'multiline::PerfLevel::SLOW'
204
+ # => 'multiline::PerfLevel::SLOW'
205
+ def replacement_from_name_span(span_text, simple_name, qualified_name)
206
+ return qualified_name if span_text == simple_name
207
+ return qualified_name if span_text.include?('::') && span_text.end_with?(simple_name)
208
+ return span_text.sub(/\A#{Regexp.escape(simple_name)}/, qualified_name) if span_text.start_with?(simple_name)
209
+
210
+ nil
211
+ end
212
+
213
+ # Find the byte offset of the declaration's top-level default-value `=`.
214
+ #
215
+ # Examples:
216
+ # 'FILE* stream = stdout'
217
+ # => offset of the `=` before stdout
218
+ #
219
+ # 'template<typename U = int> class Container = Box'
220
+ # => offset of the `=` before Box, not the inner `= int`
221
+ #
222
+ # Nested delimiters are skipped so `=` inside template parameter lists,
223
+ # function types, arrays, and braced expressions does not get mistaken for
224
+ # the declaration's own default separator.
225
+ def top_level_default_separator_offset(text)
226
+ return nil if text.nil? || text.empty?
227
+
228
+ angle_depth = 0
229
+ paren_depth = 0
230
+ bracket_depth = 0
231
+ brace_depth = 0
232
+ in_single_quote = false
233
+ in_double_quote = false
234
+ escaped = false
235
+ byte_offset = 0
236
+
237
+ text.each_char do |char|
238
+ if in_single_quote || in_double_quote
239
+ if escaped
240
+ escaped = false
241
+ elsif char == '\\'
242
+ escaped = true
243
+ elsif in_single_quote && char == "'"
244
+ in_single_quote = false
245
+ elsif in_double_quote && char == '"'
246
+ in_double_quote = false
247
+ end
248
+ else
249
+ case char
250
+ when "'"
251
+ in_single_quote = true
252
+ when '"'
253
+ in_double_quote = true
254
+ when '<'
255
+ angle_depth += 1
256
+ when '>'
257
+ angle_depth -= 1 if angle_depth > 0
258
+ when '('
259
+ paren_depth += 1
260
+ when ')'
261
+ paren_depth -= 1 if paren_depth > 0
262
+ when '['
263
+ bracket_depth += 1
264
+ when ']'
265
+ bracket_depth -= 1 if bracket_depth > 0
266
+ when '{'
267
+ brace_depth += 1
268
+ when '}'
269
+ brace_depth -= 1 if brace_depth > 0
270
+ when '='
271
+ if angle_depth.zero? && paren_depth.zero? && bracket_depth.zero? && brace_depth.zero?
272
+ return byte_offset
273
+ end
274
+ end
275
+ end
276
+
277
+ byte_offset += char.bytesize
278
+ end
279
+
280
+ nil
281
+ end
282
+
283
+ # Split a declaration at its top-level default-value '=' and return both
284
+ # the written default text and its byte offset in the source file.
285
+ #
286
+ # Examples:
287
+ # 'FILE* stream = stdout'
288
+ # => ['stdout', <offset of the s in stdout>]
289
+ #
290
+ # "PerfLevel level\n = PerfLevel::SLOW"
291
+ # => ['PerfLevel::SLOW', <offset of the P in PerfLevel::SLOW>]
292
+ #
293
+ # 'typename U = Box<Tag>'
294
+ # => ['Box<Tag>', <offset of the B in Box<Tag>>]
295
+ #
296
+ # 'template<typename U = int> class Container = Box'
297
+ # => ['Box', <offset of the B in Box>]
298
+ #
299
+ # This is text extraction only. Qualification happens later using cursor
300
+ # information so we do not lose semantic information for either function
301
+ # defaults or template parameter defaults.
302
+ def extract_default_text(param)
303
+ param_extent = param.extent.text
304
+ return nil unless param_extent
305
+
306
+ separator_offset = top_level_default_separator_offset(param_extent)
307
+ return nil unless separator_offset
308
+
309
+ before = param_extent.byteslice(0, separator_offset)
310
+ after = param_extent.byteslice((separator_offset + 1)..-1).to_s
311
+
312
+ leading_whitespace = after[/\A\s*/] || ""
313
+ default_text = after.delete_prefix(leading_whitespace)
314
+ return nil if default_text.empty?
315
+
316
+ default_text_offset = param.extent.start.offset + before.bytesize + 1 + leading_whitespace.bytesize
317
+ [default_text, default_text_offset]
318
+ end
319
+
320
+ private
321
+
322
+ # Convert a libclang source range into offsets relative to the extracted
323
+ # default-expression text so semantic replacements can be applied safely.
324
+ #
325
+ # Example:
326
+ # text = 'makePtr<inner::IndexParams>()'
327
+ # base_offset = 1200
328
+ # range = source range for 'makePtr<inner::IndexParams>'
329
+ #
330
+ # Returns offsets relative to the file:
331
+ # { start_offset: 1200, end_offset: 1228, replacement: ... }
332
+ #
333
+ # Those offsets are later converted back into indexes relative to `text`
334
+ # before patching only that exact span.
335
+ def source_range_replacement(text, base_offset, range, replacement = nil)
336
+ return nil if range.nil? || range.null?
337
+
338
+ start_offset = range.start.offset
339
+ end_offset = range.end.offset
340
+ text_end_offset = base_offset + text.bytesize
341
+ return nil if start_offset < base_offset || end_offset > text_end_offset || end_offset < start_offset
342
+
343
+ { start_offset: start_offset, end_offset: end_offset, replacement: replacement }
344
+ rescue ArgumentError
345
+ nil
346
+ end
347
+
348
+ # Apply non-overlapping source replacements from right to left so earlier
349
+ # offsets remain valid while rewriting a default expression.
350
+ #
351
+ # Example:
352
+ # text = 'helper("helper")'
353
+ # replacements = [{start_offset: ..., end_offset: ..., replacement: 'quoted::helper'}]
354
+ #
355
+ # Produces:
356
+ # 'quoted::helper("helper")'
357
+ #
358
+ # The string literal stays untouched because only the decl-ref span is replaced.
359
+ def apply_source_replacements(text, base_offset, replacements)
360
+ replacements
361
+ .uniq { |r| [r[:start_offset], r[:end_offset], r[:replacement]] }
362
+ .sort_by { |r| -r[:start_offset] }
363
+ .reduce(text.dup) do |result, replacement|
364
+ start_index = replacement[:start_offset] - base_offset
365
+ end_index = replacement[:end_offset] - base_offset
366
+ result.byteslice(0, start_index) +
367
+ replacement[:replacement] +
368
+ result.byteslice(end_index..-1).to_s
369
+ end
370
+ end
371
+
372
+ # A nested type reference can expand its start backward over the owning
373
+ # qualifier chain, producing a second replacement that starts at the same
374
+ # byte offset as the outer type reference but covers more text:
375
+ # OriginalClassName
376
+ # OriginalClassName::Params
377
+ # Keeping both corrupts the final source text, so prefer the broader span
378
+ # at a shared start offset.
379
+ def collapse_same_start_replacements(replacements)
380
+ replacements
381
+ .group_by { |replacement| replacement[:start_offset] }
382
+ .values
383
+ .map do |group|
384
+ group.max_by do |replacement|
385
+ kind_weight = replacement[:kind] == :decl ? 1 : 0
386
+ span_width = replacement[:end_offset] - replacement[:start_offset]
387
+ [kind_weight, span_width]
388
+ end
389
+ end
390
+ end
391
+
392
+ # Check whether the source text immediately before a replacement span already
393
+ # ends with `typename`, so we do not emit `typename typename Foo::Bar`.
394
+ #
395
+ # Examples:
396
+ # text = 'typename SearchIndex<Distance>::ElementType()'
397
+ # start_index = index of the S in SearchIndex
398
+ # => true
399
+ #
400
+ # text = 'SearchIndex<Distance>::ElementType()'
401
+ # start_index = index of the S in SearchIndex
402
+ # => false
403
+ def preceded_by_typename?(text, start_index)
404
+ text.byteslice(0, start_index).to_s.match?(/(?:\A|[^\w:])typename\s+\z/)
405
+ end
406
+
407
+ # Extend a name-token span backward to include a written qualifier chain.
408
+ #
409
+ # Examples:
410
+ # text = 'makePtr<inner::IndexParams>()'
411
+ # start_index = index of the I in IndexParams
412
+ # => index of the i in inner
413
+ #
414
+ # text = 'SearchIndex<Distance>::ElementType()'
415
+ # start_index = index of the E in ElementType
416
+ # => index of the S in SearchIndex
417
+ def expand_name_start_to_qualifier(text, start_index)
418
+ index = start_index
419
+
420
+ loop do
421
+ break unless index >= 2 && text.byteslice(index - 2, 2) == '::'
422
+
423
+ segment_end = index - 2
424
+ cursor = segment_end - 1
425
+ break if cursor.negative?
426
+
427
+ if text[cursor] == '>'
428
+ depth = 0
429
+ while cursor >= 0
430
+ case text[cursor]
431
+ when '>'
432
+ depth += 1
433
+ when '<'
434
+ depth -= 1
435
+ break if depth.zero?
436
+ end
437
+ cursor -= 1
438
+ end
439
+ break if cursor.negative?
440
+ cursor -= 1
441
+ end
442
+
443
+ while cursor >= 0 && text[cursor].match?(/[A-Za-z0-9_]/)
444
+ cursor -= 1
445
+ end
446
+
447
+ segment_start = cursor + 1
448
+ break if segment_start == segment_end
449
+
450
+ index = segment_start
451
+ end
452
+
453
+ index
454
+ end
455
+
456
+ def fallback_qualify_type_reference(text, ref, simple_name, qualified_name, is_dependent_typedef)
457
+ # Replace unqualified occurrences (negative lookbehind avoids already-qualified names)
458
+ result = text.gsub(/(?<!::)\b#{Regexp.escape(simple_name)}\b/, qualified_name)
459
+
460
+ # Replace partially-qualified names (e.g., flann::Foo -> cv::flann::Foo)
461
+ # Match any prefix::simple_name that isn't already fully qualified
462
+ result = result.gsub(/(?<!\w)(\w+(?:::\w+)*)::#{Regexp.escape(simple_name)}\b/) do |match|
463
+ match == qualified_name ? match : qualified_name
464
+ end
465
+
466
+ # For class template refs used as qualifiers (before ::) without explicit template args,
467
+ # insert template parameters (e.g., CompositeIndex:: -> CompositeIndex<Distance>::)
468
+ if ref.kind == :cursor_class_template
469
+ display_name = ref.qualified_display_name
470
+ result = result.gsub(/#{Regexp.escape(qualified_name)}(?=\s*::)/, display_name)
471
+ end
472
+
473
+ # Add 'typename' for dependent typedef names used as types (not as qualifiers before ::)
474
+ # e.g., SearchIndex<Distance>::ElementType() needs typename, but Vec3Type::all() does not
475
+ if is_dependent_typedef
476
+ result = result.gsub(/(?<!typename )#{Regexp.escape(qualified_name)}(?!\s*::)/, "typename #{qualified_name}")
477
+ end
478
+
479
+ result
480
+ end
481
+
482
+ def fallback_qualify_declaration_reference(text, simple_name, qualified_name)
483
+ # Apply qualification (negative lookbehind avoids double-qualifying)
484
+ result = text.gsub(/(?<!::)\b#{Regexp.escape(simple_name)}\b/, qualified_name)
485
+
486
+ # Replace partially-qualified names (e.g., fisheye::CALIB_FIX_INTRINSIC -> cv::fisheye::CALIB_FIX_INTRINSIC)
487
+ # Match any prefix::simple_name that isn't already fully qualified
488
+ result.gsub(/(?<!\w)(\w+(?:::\w+)*)::#{Regexp.escape(simple_name)}\b/) do |match|
489
+ match == qualified_name ? match : qualified_name
490
+ end
491
+ end
492
+ end
493
+ end
494
+ end
495
+ end