rubylens 0.1.0 → 0.2.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.
@@ -10,28 +10,24 @@ module RubyLens
10
10
  module Index
11
11
  class RubydexAdapter
12
12
  TEST_SEGMENTS = %w[test tests spec specs feature features].freeze
13
+ CONSTANT_REFERENCE_LINK_LIMIT = 1_024
14
+ CONSTANT_REFERENCE_ATTRIBUTION_LIMIT = CONSTANT_REFERENCE_LINK_LIMIT * 2
13
15
 
14
- # Everything the collectors need from one declaration's definitions, gathered in a
15
- # single pass so each definition's location crosses the Rubydex boundary once.
16
- DefinitionSummary = Data.define(
17
- :workspace_count,
18
- :workspace_relatives,
19
- :canonical_relatives,
20
- :canonical_site_keys,
21
- :package_site_keys,
22
- )
16
+ ReferenceSummary = Data.define(:inbound_counts, :links)
23
17
 
24
18
  def initialize(manifest)
25
19
  @manifest = manifest
26
20
  @source_path_cache = {}
27
21
  @workspace_location_cache = {}
22
+ @definition_scope_by_uri = {}
23
+ @package_index_by_uri = {}
28
24
  @indexed_package_document_paths = Set.new
29
25
  end
30
26
 
31
27
  def index
32
28
  graph = Rubydex::Graph.new(workspace_path: @manifest.root.to_s)
33
29
  index_errors = graph.index_all(@manifest.files)
34
- @indexed_package_document_paths = indexed_package_document_paths(graph)
30
+ @indexed_package_document_paths, documents_with_paths = resolve_documents(graph)
35
31
  graph.resolve
36
32
  integrity_failures = graph.check_integrity
37
33
  collected = collect_declarations(graph.declarations)
@@ -39,22 +35,31 @@ module RubyLens
39
35
  graph: graph,
40
36
  manifest: @manifest,
41
37
  package_document_paths: @indexed_package_document_paths,
38
+ documents_with_paths: documents_with_paths,
42
39
  ).call
43
- workspace = workspace_namespaces(collected.fetch(:workspace_records), rspec.groups)
44
- inbound_references = inbound_workspace_references(graph, workspace.fetch(:ordinal_by_name))
40
+ workspace = workspace_namespaces(
41
+ collected.fetch(:workspace_records),
42
+ rspec.groups,
43
+ collected.fetch(:workspace_definition_ranges),
44
+ )
45
+ reference_summary = workspace_reference_summary(
46
+ graph,
47
+ workspace,
48
+ collected.fetch(:dependency_ordinal_by_name),
49
+ )
45
50
  category_stats = collected.fetch(:category_stats)
46
51
  category_stats.fetch("tests")[0] += rspec.groups.length
47
52
  category_stats.fetch("tests")[2] += rspec.method_count
48
53
 
49
54
  {
50
- "schema" => "rubylens.snapshot.v6",
55
+ "schema" => "rubylens.snapshot.v9",
51
56
  "project_name" => project_name,
52
- "components" => workspace.fetch(:component_counts),
53
57
  "namespace_names" => workspace.fetch(:namespace_names),
54
- "namespaces" => build_workspace_rows(workspace, inbound_references),
58
+ "namespaces" => build_workspace_rows(workspace, reference_summary.inbound_counts),
59
+ "constant_reference_links" => reference_summary.links,
55
60
  "category_stats" => category_stats,
56
- "dependency_signal_maxima" => collected.fetch(:dependency_aggregation).signal_maxima,
57
- "packages" => build_package_rows(collected.fetch(:dependency_aggregation)),
61
+ "dependency_signal_maxima" => collected.fetch(:dependency_signal_maxima),
62
+ "packages" => build_package_rows(collected.fetch(:dependency_packages)),
58
63
  "dependency_systems" => build_dependency_system_rows,
59
64
  "dependency_warnings" => @manifest.respond_to?(:dependency_warnings) ? @manifest.dependency_warnings : [],
60
65
  "warning_counts" => {
@@ -71,86 +76,138 @@ module RubyLens
71
76
  records = []
72
77
  category_stats = { "core" => Array.new(4, 0), "tests" => Array.new(4, 0) }
73
78
  aggregation = Model::DependencyAggregation.new(package_count: @manifest.packages.length)
79
+ workspace_definition_ranges = Hash.new { |hash, uri| hash[uri] = [] }
80
+ dependency_positions = {}
74
81
 
75
82
  declarations.each do |declaration|
76
- next unless model_eligible_declaration?(declaration)
77
-
78
- summary = summarize_definitions(declaration)
79
- unless summary.canonical_site_keys.empty?
80
- records << [
81
- declaration,
82
- summary.canonical_site_keys.uniq.length,
83
- scope_from(summary.canonical_relatives),
84
- component_from(summary.canonical_relatives),
85
- ]
83
+ name = declaration.name
84
+ next unless model_eligible_declaration?(declaration, name)
85
+
86
+ is_namespace = declaration.is_a?(Rubydex::Namespace)
87
+ construct_index = ruby_construct_index(declaration)
88
+ workspace_count, tests_only, canonical_site_keys, canonical_scope, package_site_keys =
89
+ summarize_definitions(declaration, is_namespace)
90
+ reference_count = safe_length(declaration, :references) if package_site_keys
91
+
92
+ if canonical_site_keys
93
+ canonical_site_keys = canonical_site_keys.uniq if canonical_site_keys.length > 1
94
+ ordinal = records.length
95
+ canonical_site_keys.each do |uri, start_line, start_column, end_line, end_column|
96
+ workspace_definition_ranges[uri] << [start_line, start_column, end_line, end_column, ordinal]
97
+ end
98
+ records << [declaration, canonical_site_keys.length, canonical_scope, name]
86
99
  end
87
- collect_category_stat(category_stats, declaration, summary)
88
- collect_dependency_declaration(aggregation, declaration, summary)
100
+ if construct_index && workspace_count.positive?
101
+ category_stats.fetch(tests_only ? "tests" : "core")[construct_index] += 1
102
+ end
103
+ collect_dependency_declaration(
104
+ aggregation,
105
+ dependency_positions,
106
+ declaration,
107
+ name,
108
+ is_namespace,
109
+ construct_index,
110
+ package_site_keys,
111
+ reference_count,
112
+ )
89
113
  end
90
114
 
91
- { workspace_records: records, category_stats:, dependency_aggregation: aggregation }
115
+ dependency_packages = aggregation.packages
116
+ dependency_offsets = []
117
+ offset = 0
118
+ dependency_packages.each do |package|
119
+ dependency_offsets << offset
120
+ offset += package.fetch(:declarations).length
121
+ end
122
+ dependency_ordinal_by_name = dependency_positions.transform_values! do |position|
123
+ package_index, local_index = position
124
+ dependency_offsets.fetch(package_index) + local_index
125
+ end
126
+
127
+ {
128
+ workspace_records: records,
129
+ workspace_definition_ranges:,
130
+ category_stats:,
131
+ dependency_packages:,
132
+ dependency_signal_maxima: aggregation.signal_maxima,
133
+ dependency_ordinal_by_name: dependency_ordinal_by_name.freeze,
134
+ }
92
135
  end
93
136
 
94
- def summarize_definitions(declaration)
95
- namespace = namespace?(declaration)
137
+ # One pass over a declaration's definitions gathers everything the
138
+ # collectors need, so each definition's location and its per-URI
139
+ # workspace/scope/package answers cross the Rubydex boundary once.
140
+ # Canonical site keys and package site keys stay nil until a definition
141
+ # actually contributes to them, since most declarations produce neither.
142
+ def summarize_definitions(declaration, is_namespace)
96
143
  workspace_count = 0
97
- workspace_relatives = []
98
- canonical_relatives = []
99
- canonical_site_keys = []
100
- package_site_keys = {}
144
+ tests_seen = false
145
+ core_seen = false
146
+ canonical_tests_seen = false
147
+ canonical_core_seen = false
148
+ canonical_site_keys = nil
149
+ package_site_keys = nil
101
150
 
102
151
  declaration.definitions.each do |definition|
103
152
  location = definition.location
153
+ site_key = nil
104
154
  if workspace_location?(location)
105
155
  workspace_count += 1
106
- relative = @manifest.relative_workspace_path(source_path(location.uri))
107
- workspace_relatives << relative if relative
108
- if namespace && canonical_namespace_definition?(declaration, definition)
109
- canonical_site_keys << site_key(location)
110
- canonical_relatives << relative if relative
156
+ scope = definition_scope(location.uri)
157
+ if scope == 1
158
+ tests_seen = true
159
+ elsif scope
160
+ core_seen = true
161
+ end
162
+ if is_namespace && canonical_namespace_definition?(declaration, definition)
163
+ (canonical_site_keys ||= []) << (site_key = location.comparable_values)
164
+ if scope == 1
165
+ canonical_tests_seen = true
166
+ elsif scope
167
+ canonical_core_seen = true
168
+ end
111
169
  end
112
170
  end
113
171
  package_index = package_index_for_location(location)
114
- (package_site_keys[package_index] ||= []) << site_key(location) if package_index
172
+ if package_index
173
+ ((package_site_keys ||= {})[package_index] ||= []) << (site_key || location.comparable_values)
174
+ end
115
175
  end
116
176
 
117
- DefinitionSummary.new(
118
- workspace_count:,
119
- workspace_relatives:,
120
- canonical_relatives:,
121
- canonical_site_keys:,
122
- package_site_keys:,
123
- )
177
+ canonical_scope = canonical_tests_seen ? (canonical_core_seen ? 2 : 1) : 0
178
+ [workspace_count, tests_seen && !core_seen, canonical_site_keys, canonical_scope, package_site_keys]
124
179
  end
125
180
 
126
- def workspace_namespaces(records, rspec_groups)
127
- ordinal_by_name = records.each_with_index.to_h { |(declaration, *), index| [declaration.name, index] }
128
- components = records.map { |_declaration, _sites, _scope, component| component }
129
- all_components = components + rspec_groups.map(&:component)
130
- component_ids = all_components.uniq.sort.each_with_index.to_h
131
-
181
+ def workspace_namespaces(records, rspec_groups, definition_ranges = {})
182
+ namespace_names = []
183
+ ordinal_by_name = {}
184
+ records.each_with_index do |record, index|
185
+ name = record.fetch(3)
186
+ namespace_names << name
187
+ ordinal_by_name[name] = index
188
+ end
132
189
  {
133
190
  records: records,
191
+ definition_ranges: definition_ranges,
134
192
  rspec_groups: rspec_groups,
135
- namespace_names: records.map { |declaration, *| declaration.name } + rspec_groups.map(&:name),
193
+ namespace_names: namespace_names + rspec_groups,
136
194
  ordinal_by_name: ordinal_by_name,
137
- component_ids: component_ids,
138
- components: components,
139
- component_counts: all_components.tally.sort_by { |name, _count| component_ids.fetch(name) }.map(&:last),
140
195
  }
141
196
  end
142
197
 
143
198
  def build_workspace_rows(workspace, inbound_references)
144
- rows = workspace.fetch(:records).each_with_index.map do |(declaration, sites, scope, component), index|
199
+ ordinal_by_name = workspace.fetch(:ordinal_by_name)
200
+ rows = workspace.fetch(:records).each_with_index.map do |record, index|
201
+ declaration, sites, scope, name = record
145
202
  descendants = declaration.descendants.count do |descendant|
146
- descendant.name != declaration.name && workspace.fetch(:ordinal_by_name).key?(descendant.name)
203
+ descendant_name = descendant.name
204
+ descendant_name != name && ordinal_by_name.key?(descendant_name)
147
205
  end
148
206
  member_count, ruby_counts, instance_variable_count = member_statistics(
149
207
  declaration,
150
208
  count_instance_variables: declaration.is_a?(Rubydex::Class) && scope != 1,
151
209
  )
152
210
  [
153
- workspace.fetch(:component_ids).fetch(component),
154
211
  declaration.is_a?(Rubydex::Class) ? 0 : 1,
155
212
  scope,
156
213
  [declaration.ancestors.count - 1, 0].max,
@@ -163,27 +220,20 @@ module RubyLens
163
220
  instance_variable_count,
164
221
  ]
165
222
  end
166
- rows.concat(workspace.fetch(:rspec_groups).map { |group| build_rspec_row(group, workspace) })
223
+ rows.concat(workspace.fetch(:rspec_groups).map { build_rspec_row })
167
224
  end
168
225
 
169
- def build_rspec_row(group, workspace)
170
- [
171
- workspace.fetch(:component_ids).fetch(group.component),
172
- 0,
173
- 1,
174
- *Array.new(11, 0),
175
- ]
226
+ def build_rspec_row
227
+ [0, 1, *Array.new(11, 0)]
176
228
  end
177
229
 
178
- def build_package_rows(aggregation)
179
- aggregates = aggregation.packages
230
+ def build_package_rows(aggregates)
180
231
  @manifest.packages.each_with_index.map do |package, index|
181
232
  aggregate = aggregates.fetch(index)
182
233
  {
183
234
  "name" => package.name,
184
235
  "role" => package.role == "direct" ? 0 : 1,
185
236
  "location" => package.location == "workspace" ? 0 : 1,
186
- "declaration_count" => aggregate.fetch(:declaration_count),
187
237
  "ruby_counts" => aggregate.fetch(:ruby_counts),
188
238
  "declarations" => aggregate.fetch(:declarations),
189
239
  }
@@ -202,62 +252,173 @@ module RubyLens
202
252
  end
203
253
  end
204
254
 
205
- def collect_dependency_declaration(aggregation, declaration, summary)
206
- package_index, site_keys = summary.package_site_keys.max_by do |index, keys|
207
- [keys.length, -index]
255
+ def collect_dependency_declaration(
256
+ aggregation,
257
+ positions,
258
+ declaration,
259
+ name,
260
+ is_namespace,
261
+ construct_index,
262
+ package_site_keys,
263
+ reference_count
264
+ )
265
+ return unless package_site_keys
266
+
267
+ package_index = nil
268
+ site_keys = nil
269
+ package_site_keys.each do |index, keys|
270
+ if site_keys.nil? || keys.length > site_keys.length ||
271
+ (keys.length == site_keys.length && index < package_index)
272
+ package_index = index
273
+ site_keys = keys
274
+ end
208
275
  end
209
- return unless package_index
210
276
 
211
- sites = site_keys.uniq.length
277
+ sites = site_keys.length > 1 ? site_keys.uniq.length : site_keys.length
212
278
  row = [
213
279
  namespace_kind(declaration),
214
- namespace?(declaration) ? [declaration.ancestors.count - 1, 0].max : 0,
280
+ is_namespace ? [declaration.ancestors.count - 1, 0].max : 0,
215
281
  sites,
216
282
  [sites - 1, 0].max,
217
- namespace?(declaration) ? [declaration.descendants.count - 1, 0].max : 0,
218
- safe_length(declaration, :references),
219
- namespace?(declaration) ? safe_length(declaration, :members) : 0,
220
- ]
221
- aggregation.add(
283
+ is_namespace ? [declaration.descendants.count - 1, 0].max : 0,
284
+ reference_count,
285
+ is_namespace ? safe_length(declaration, :members) : 0,
286
+ ].freeze
287
+ local_index = aggregation.add(
222
288
  package_index:,
223
289
  row:,
224
- construct_index: ruby_construct_index(declaration),
290
+ construct_index:,
225
291
  )
292
+ if reference_count.positive? && constant_reference_target?(declaration, is_namespace)
293
+ positions[name] = [package_index, local_index]
294
+ end
226
295
  end
227
296
 
228
- def inbound_workspace_references(graph, ordinal_by_name)
229
- graph.constant_references.each_with_object(Hash.new(0)) do |reference, counts|
297
+ def workspace_reference_summary(graph, workspace, dependency_ordinal_by_name = {})
298
+ ordinal_by_name = workspace.fetch(:ordinal_by_name)
299
+ namespace_count = workspace.fetch(:namespace_names, ordinal_by_name).length
300
+ workspace_ranges = workspace.fetch(:definition_ranges, {})
301
+ inbound_counts = Hash.new(0)
302
+ workspace.fetch(:records).each_with_index do |record, index|
303
+ record.fetch(0).references.each do |reference|
304
+ location = begin
305
+ reference.location
306
+ rescue StandardError
307
+ next
308
+ end
309
+ inbound_counts[index] += 1 if workspace_location?(location)
310
+ end
311
+ end
312
+ links = []
313
+ retained_links = Set.new
314
+ attribution_count = 0
315
+
316
+ graph.constant_references.each do |reference|
317
+ break if links.length >= CONSTANT_REFERENCE_LINK_LIMIT ||
318
+ attribution_count >= CONSTANT_REFERENCE_ATTRIBUTION_LIMIT
230
319
  next unless reference.is_a?(Rubydex::ResolvedConstantReference)
231
320
 
232
- ordinal = ordinal_by_name[reference.declaration.name]
233
- next unless ordinal
321
+ referenced_name = resolved_constant_reference_name(reference)
322
+ next unless referenced_name
323
+ referenced_ordinal = ordinal_by_name[referenced_name]
324
+ dependency_ordinal = dependency_ordinal_by_name[referenced_name] unless referenced_ordinal
325
+ next unless referenced_ordinal || dependency_ordinal
326
+
327
+ location = begin
328
+ reference.location
329
+ rescue StandardError
330
+ next
331
+ end
332
+ next unless workspace_location?(location)
333
+
334
+ attribution_count += 1
335
+
336
+ referenced_endpoint = referenced_ordinal || namespace_count + dependency_ordinal
337
+
338
+ location_values = begin
339
+ location.comparable_values
340
+ rescue StandardError
341
+ next
342
+ end
343
+ referring_ordinal = enclosing_definition_ordinal(location_values, workspace_ranges)
344
+ next unless referring_ordinal
345
+ next if referenced_endpoint == referring_ordinal
346
+
347
+ link = [referring_ordinal, referenced_endpoint]
348
+ next unless retained_links.add?(link)
234
349
 
235
- counts[ordinal] += 1 if workspace_location?(reference.location)
236
- rescue StandardError
237
- next
350
+ links << link
238
351
  end
352
+
353
+ ReferenceSummary.new(inbound_counts.freeze, links.freeze)
239
354
  end
240
355
 
241
- def package_index_for_location(location)
242
- path = source_path(location.uri)
243
- return nil unless path
244
- return nil unless @indexed_package_document_paths.include?(path)
356
+ def resolved_constant_reference_name(reference)
357
+ reference.declaration.name
358
+ rescue StandardError
359
+ nil
360
+ end
361
+
362
+ def enclosing_definition_ordinal(location_values, ranges_by_uri)
363
+ uri, start_line, start_column, end_line, end_column = location_values
364
+ best = nil
365
+ ambiguous = false
366
+ ranges_by_uri.fetch(uri, []).each do |range|
367
+ range_start_line, range_start_column, range_end_line, range_end_column, ordinal = range
368
+ next if position_compare(range_start_line, range_start_column, start_line, start_column).positive?
369
+ next if position_compare(range_end_line, range_end_column, end_line, end_column).negative?
370
+
371
+ start_comparison = best ? position_compare(range_start_line, range_start_column, best[0], best[1]) : 1
372
+ end_comparison = best && start_comparison.zero? ? position_compare(range_end_line, range_end_column, best[2], best[3]) : 0
373
+ if start_comparison.positive? || (start_comparison.zero? && end_comparison.negative?)
374
+ best = range
375
+ ambiguous = false
376
+ elsif start_comparison.zero? && end_comparison.zero? && ordinal != best[4]
377
+ ambiguous = true
378
+ end
379
+ end
245
380
 
246
- @manifest.package_index_for(path)
381
+ ambiguous ? nil : best&.fetch(4)
382
+ end
383
+
384
+ def position_compare(left_line, left_column, right_line, right_column)
385
+ line_comparison = left_line <=> right_line
386
+ line_comparison.zero? ? left_column <=> right_column : line_comparison
387
+ end
388
+
389
+ def package_index_for_location(location)
390
+ uri = location.uri
391
+ @package_index_by_uri.fetch(uri) do
392
+ path = source_path(uri)
393
+ @package_index_by_uri[uri] =
394
+ if path && @indexed_package_document_paths.include?(path)
395
+ @manifest.package_index_for(path)
396
+ end
397
+ end
247
398
  end
248
399
 
249
- def indexed_package_document_paths(graph)
400
+ # One sweep over the indexed documents resolves each URI once, yielding
401
+ # both the package-audit set and the [document, path] pairs the RSpec
402
+ # extractor reuses without re-enumerating documents or re-parsing URIs.
403
+ def resolve_documents(graph)
250
404
  audited = @manifest.packages.flat_map(&:files).to_set
251
- graph.documents.each_with_object(Set.new) do |document, paths|
405
+ package_paths = Set.new
406
+ documents_with_paths = []
407
+ graph.documents.each do |document|
252
408
  path = source_path(document.uri)
253
- paths << path if path && audited.include?(path)
409
+ next unless path
410
+
411
+ documents_with_paths << [document, path]
412
+ package_paths << path if audited.include?(path)
254
413
  end
414
+ [package_paths, documents_with_paths]
255
415
  end
256
416
 
257
417
  # One sweep over a namespace's direct and singleton members yields the
258
418
  # workspace member count, the method/constant construct counts, and the
259
419
  # instance-variable count that previously took three walks each re-reading
260
- # every member's definitions.
420
+ # every member's definitions. Membership comes from each member's own
421
+ # definition locations; the per-URI caches keep that scan cheap.
261
422
  def member_statistics(declaration, count_instance_variables:)
262
423
  member_count = 0
263
424
  instance_variable_count = 0
@@ -266,7 +427,10 @@ module RubyLens
266
427
  ruby_counts[own_construct] += 1 if own_construct == 0 || own_construct == 1
267
428
 
268
429
  seen_names = Set.new
269
- walk = lambda do |members, direct|
430
+ member_groups = [[declaration.members, true]]
431
+ singleton = declaration.singleton_class
432
+ member_groups << [singleton.members, false] if singleton
433
+ member_groups.each do |members, direct|
270
434
  members.each do |member|
271
435
  next unless seen_names.add?(member.name)
272
436
  next unless member.definitions.any? { |definition| workspace_location?(definition.location) }
@@ -279,60 +443,46 @@ module RubyLens
279
443
  end
280
444
  end
281
445
  end
282
- walk.call(declaration.members, true)
283
- walk.call(declaration.singleton_class.members, false) if declaration.singleton_class
284
446
  [member_count, ruby_counts, instance_variable_count]
285
447
  rescue StandardError
286
448
  [member_count, ruby_counts, instance_variable_count]
287
449
  end
288
450
 
289
- def collect_category_stat(stats, declaration, summary)
290
- construct_index = ruby_construct_index(declaration)
291
- return unless construct_index
292
- return if summary.workspace_count.zero?
293
-
294
- category = scope_from(summary.workspace_relatives) == 1 ? "tests" : "core"
295
- stats.fetch(category)[construct_index] += 1
296
- end
297
-
298
451
  def workspace_location?(location)
299
452
  uri = location.uri
300
- return @workspace_location_cache[uri] if @workspace_location_cache.key?(uri)
301
-
302
- path = source_path(uri)
303
- @workspace_location_cache[uri] = path ? @manifest.workspace_path?(path) : false
453
+ @workspace_location_cache.fetch(uri) do
454
+ path = source_path(uri)
455
+ @workspace_location_cache[uri] = path ? @manifest.workspace_path?(path) : false
456
+ end
304
457
  rescue StandardError
305
458
  false
306
459
  end
307
460
 
308
- def component_from(relatives)
309
- candidates = relatives.map { |relative| SourcePath.component_for(relative) }
310
- candidates.tally.max_by { |name, count| [count, name] }&.first || "root"
311
- end
312
-
313
- def scope_from(relatives)
314
- scopes = relatives.map do |relative|
315
- relative.split(File::SEPARATOR).any? { |segment| TEST_SEGMENTS.include?(segment) } ? 1 : 0
316
- end.uniq
317
- scopes.length > 1 ? 2 : scopes.first || 0
318
- end
319
-
320
- def site_key(location)
321
- location.comparable_values
461
+ # 1 for a test-tree file, 0 for any other workspace file, nil when the
462
+ # URI has no workspace-relative path to classify.
463
+ def definition_scope(uri)
464
+ @definition_scope_by_uri.fetch(uri) do
465
+ relative = @manifest.relative_workspace_path(source_path(uri))
466
+ @definition_scope_by_uri[uri] =
467
+ if relative
468
+ relative.split(File::SEPARATOR).any? { |segment| TEST_SEGMENTS.include?(segment) } ? 1 : 0
469
+ end
470
+ end
322
471
  end
323
472
 
324
- def namespace?(declaration)
325
- declaration.is_a?(Rubydex::Namespace)
473
+ def constant_reference_target?(declaration, is_namespace)
474
+ is_namespace ||
475
+ declaration.is_a?(Rubydex::Constant) ||
476
+ declaration.is_a?(Rubydex::ConstantAlias)
326
477
  end
327
478
 
328
- def model_eligible_declaration?(declaration)
329
- name = declaration.name
479
+ def model_eligible_declaration?(declaration, name = declaration.name)
330
480
  return false if name.nil? || name.empty? || name.include?("<anonymous>")
331
481
  return false if declaration.is_a?(Rubydex::Todo)
332
482
  return true unless declaration.is_a?(Rubydex::SingletonClass)
333
483
 
334
- !declaration.attached_class.is_a?(Rubydex::SingletonClass) &&
335
- !declaration.attached_class.is_a?(Rubydex::Todo)
484
+ attached = declaration.attached_class
485
+ !attached.is_a?(Rubydex::SingletonClass) && !attached.is_a?(Rubydex::Todo)
336
486
  end
337
487
 
338
488
  def canonical_namespace_definition?(declaration, definition)
@@ -359,9 +509,9 @@ module RubyLens
359
509
  end
360
510
 
361
511
  def source_path(uri_string)
362
- return @source_path_cache[uri_string] if @source_path_cache.key?(uri_string)
363
-
364
- @source_path_cache[uri_string] = SourcePath.from_file_uri(uri_string)
512
+ @source_path_cache.fetch(uri_string) do
513
+ @source_path_cache[uri_string] = SourcePath.from_file_uri(uri_string)
514
+ end
365
515
  end
366
516
 
367
517
  def safe_length(object, method)
@@ -6,13 +6,24 @@ module RubyLens
6
6
  module Index
7
7
  # Maps Rubydex document and location URIs back to workspace source paths.
8
8
  module SourcePath
9
- COMPONENT_ROOTS = %w[lib app test tests spec specs].freeze
9
+ # The canonical shape Rubydex emits: an empty authority and a
10
+ # percent-encoded absolute path built from RFC 2396 path characters.
11
+ # Anything else falls back to full URI parsing below.
12
+ ENCODED_FILE_URI = %r{\Afile://(/(?:[A-Za-z0-9\-_.!~*'()/:@&=+$,;]|%[0-9A-Fa-f]{2})*)\z}
10
13
 
11
14
  module_function
12
15
 
13
16
  # Rubydex 0.2.9 percent-encodes file URIs and Location#to_file_path
14
17
  # returns the path still encoded, so decode the URI ourselves.
15
18
  def from_file_uri(uri_string)
19
+ match = ENCODED_FILE_URI.match(uri_string)
20
+ if match
21
+ path = match[1]
22
+ path = URI::RFC2396_PARSER.unescape(path) if path.include?("%")
23
+ path = path.delete_prefix("/") if Gem.win_platform?
24
+ return path
25
+ end
26
+
16
27
  uri = URI.parse(uri_string)
17
28
  return unless uri.scheme == "file"
18
29
  return if uri.host && !uri.host.empty? && uri.host != "localhost"
@@ -24,16 +35,6 @@ module RubyLens
24
35
  rescue URI::InvalidURIError
25
36
  nil
26
37
  end
27
-
28
- def component_for(relative)
29
- segments = relative.split(File::SEPARATOR)
30
- first = segments.first || "root"
31
- if COMPONENT_ROOTS.include?(first)
32
- "#{first}/#{segments[1] || "root"}"
33
- else
34
- first
35
- end
36
- end
37
38
  end
38
39
  end
39
40
  end