rubylens 0.2.0 → 0.2.1
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 +4 -4
- data/README.md +3 -3
- data/lib/rubylens/art_model_builder.rb +184 -94
- data/lib/rubylens/index/constant_reference_collector.rb +150 -0
- data/lib/rubylens/index/declaration_collector.rb +288 -0
- data/lib/rubylens/index/git_package_source.rb +202 -0
- data/lib/rubylens/index/location_index.rb +103 -0
- data/lib/rubylens/index/manifest.rb +54 -150
- data/lib/rubylens/index/rubydex_adapter.rb +109 -449
- data/lib/rubylens/model/dependency_aggregation.rb +3 -1
- data/lib/rubylens/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: abf1b3d5b3080d2f7a6715fe84c770cdd184ab1c164da45e9d9b4f0c3c1c44f6
|
|
4
|
+
data.tar.gz: 966f347550c546b49bfa70def65d375f3cb5675c804ed52cd2328b182c45dd82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fdb436f8272c226cbb020851269c04de5c3b713d0eb640b05580ef8a4f7930c5bee8c0df9da0d3185877da452895a2e94e47683f9cb8bf33d884ec80d21d7990
|
|
7
|
+
data.tar.gz: 27f66613b7207f462d180333f8da247574c7b46064bc44715e3d3914c6b1b25c38f46cda2f5434b1940c64280b7f54c67d4fcc61a9514aec90ead2fa326cd337
|
data/README.md
CHANGED
|
@@ -28,11 +28,11 @@ Three levels of disclosure:
|
|
|
28
28
|
> [!IMPORTANT]
|
|
29
29
|
> Nothing is uploaded and no source code is embedded. Outputs still describe your project: they name it, can name classes and gems, and reveal some relationship topology. See [Privacy and sharing](#privacy-and-sharing).
|
|
30
30
|
|
|
31
|
-
https://github.com/user-attachments/assets/
|
|
31
|
+
https://github.com/user-attachments/assets/ec8d9357-c726-463b-bbde-0fd6eca25d2c
|
|
32
32
|
|
|
33
|
-
*The Explorer on
|
|
33
|
+
*The Explorer on Discourse: search, fly to a class, expand a gem cloud.*
|
|
34
34
|
|
|
35
|
-
https://github.com/user-attachments/assets/
|
|
35
|
+
https://github.com/user-attachments/assets/972a15b9-d863-4a52-85e1-af7bc92c0459
|
|
36
36
|
|
|
37
37
|
*A `--details` clip: what your team sees.*
|
|
38
38
|
|
|
@@ -6,113 +6,202 @@ require_relative "morphology_classifier"
|
|
|
6
6
|
|
|
7
7
|
module RubyLens
|
|
8
8
|
class ArtModelBuilder
|
|
9
|
+
SCHEMA = "rubylens.art.v13"
|
|
9
10
|
SIGNAL_FIELDS = %w[ancestorDepth definitionSites reopenings descendants references members].freeze
|
|
11
|
+
# Namespace row columns carrying the six signals, aligned with SIGNAL_FIELDS.
|
|
12
|
+
NAMESPACE_SIGNAL_COLUMNS = (3..8).freeze
|
|
10
13
|
|
|
14
|
+
# `order` lists snapshot ordinals in draw order and is the single authority
|
|
15
|
+
# for each shuffle; the snapshot-ordinal-to-draw-position inverse is derived
|
|
16
|
+
# where it is needed rather than stored beside it.
|
|
17
|
+
Namespaces = Data.define(
|
|
18
|
+
:order, #: Array[Integer]
|
|
19
|
+
:rows, #: Array[Array[Integer]]
|
|
20
|
+
:names, #: Array[String]
|
|
21
|
+
:reference_rows, #: Array[[Integer, Integer]]
|
|
22
|
+
) do
|
|
23
|
+
# Link endpoints past the namespace block address dependency stars, which
|
|
24
|
+
# the snapshot numbers from zero after the namespaces.
|
|
25
|
+
#
|
|
26
|
+
#: () -> Array[Integer]
|
|
27
|
+
def referenced_dependency_ordinals
|
|
28
|
+
reference_rows.map { |_referring_index, referenced_index| referenced_index - order.length }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#: () -> Array[Integer]
|
|
32
|
+
def draw_positions
|
|
33
|
+
positions = Array.new(order.length)
|
|
34
|
+
order.each_with_index { |old_index, new_index| positions[old_index] = new_index }
|
|
35
|
+
positions
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Packages = Data.define(
|
|
40
|
+
:order, #: Array[Integer]
|
|
41
|
+
:rows, #: Array[Array[Integer]]
|
|
42
|
+
:names, #: Array[String]
|
|
43
|
+
:morphologies, #: Array[Array[Integer]]
|
|
44
|
+
:systems, #: Array[Array[Integer]]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Unlike the other two, this index is not an inverse of any stored order:
|
|
48
|
+
# a dependency star's snapshot ordinal is only known for rows travel
|
|
49
|
+
# targets, and is captured as the shuffled rows are emitted.
|
|
50
|
+
Dependencies = Data.define(
|
|
51
|
+
:rows, #: Array[Array[Integer]]
|
|
52
|
+
:index, #: Hash[Integer, Integer]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
#: (?seed: Integer) -> void
|
|
11
56
|
def initialize(seed: 0x51A7_E11A)
|
|
12
57
|
@seed = seed
|
|
13
58
|
end
|
|
14
59
|
|
|
60
|
+
# Draw order is shuffled so neighbouring stars come from unrelated parts of
|
|
61
|
+
# the codebase: adjacent points would otherwise share a package or a name
|
|
62
|
+
# prefix and read as structure the galaxy does not mean to show. Every
|
|
63
|
+
# ordinal the snapshot used is therefore remapped onto the shuffled order.
|
|
64
|
+
#: (Hash[String, untyped]) -> Hash[String, untyped]
|
|
15
65
|
def build(snapshot)
|
|
16
66
|
random = Random.new(@seed)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
67
|
+
namespaces = build_namespaces(snapshot, random)
|
|
68
|
+
packages = build_packages(snapshot, random)
|
|
69
|
+
dependencies = build_dependencies(snapshot, packages, namespaces, random)
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
"schema" => SCHEMA,
|
|
73
|
+
"projectName" => snapshot.fetch("project_name"),
|
|
74
|
+
"morphology" => morphology_row(MorphologyClassifier.new(snapshot).call),
|
|
75
|
+
"totals" => {
|
|
76
|
+
"namespaces" => namespaces.rows.length,
|
|
77
|
+
"packages" => packages.rows.length,
|
|
78
|
+
"dependencyStars" => dependencies.rows.length,
|
|
79
|
+
},
|
|
80
|
+
"domains" => signal_domains(namespaces.rows, snapshot.fetch("dependency_signal_maxima")),
|
|
81
|
+
"categoryStats" => snapshot.fetch("category_stats"),
|
|
82
|
+
"namespaceNames" => namespaces.names,
|
|
83
|
+
"namespaces" => namespaces.rows,
|
|
84
|
+
"constantReferenceLinks" => build_constant_reference_links(
|
|
85
|
+
namespaces.reference_rows,
|
|
86
|
+
namespaces.draw_positions,
|
|
87
|
+
dependencies.index,
|
|
88
|
+
),
|
|
89
|
+
"packageNames" => packages.names,
|
|
90
|
+
"packages" => packages.rows,
|
|
91
|
+
"packageMorphologies" => packages.morphologies,
|
|
92
|
+
"dependencySystems" => packages.systems,
|
|
93
|
+
"dependencyStars" => dependencies.rows,
|
|
94
|
+
"dependencyWarnings" => allowed_dependency_warnings(snapshot),
|
|
95
|
+
"warningCounts" => snapshot.fetch("warning_counts"),
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
#: (Hash[String, untyped], Random) -> Namespaces
|
|
102
|
+
def build_namespaces(snapshot, random)
|
|
103
|
+
rows = snapshot.fetch("namespaces")
|
|
104
|
+
names = snapshot.fetch("namespace_names")
|
|
105
|
+
order = (0...rows.length).to_a.shuffle(random: random)
|
|
106
|
+
Namespaces.new(
|
|
107
|
+
order: order,
|
|
108
|
+
rows: order.map { |old_index| [random.rand(0..0xffff_ffff), *rows.fetch(old_index)] },
|
|
109
|
+
names: order.map { |old_index| names.fetch(old_index) },
|
|
110
|
+
reference_rows: valid_constant_reference_rows(snapshot),
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
#: (Hash[String, untyped], Random) -> Packages
|
|
115
|
+
def build_packages(snapshot, random)
|
|
116
|
+
snapshot_packages = snapshot.fetch("packages")
|
|
117
|
+
order = (0...snapshot_packages.length).to_a.shuffle(random: random)
|
|
118
|
+
systems, system_index = build_dependency_systems(snapshot, order.each_with_index.to_h)
|
|
119
|
+
morphologies = []
|
|
120
|
+
|
|
121
|
+
rows = order.map do |old_index|
|
|
122
|
+
package = snapshot_packages.fetch(old_index)
|
|
123
|
+
seed = random.rand(0..0xffff_ffff)
|
|
124
|
+
morphologies << morphology_row(MorphologyClassifier.new(package: package, phase_seed: seed).call)
|
|
36
125
|
[
|
|
37
|
-
|
|
126
|
+
seed,
|
|
38
127
|
package.fetch("role"),
|
|
39
128
|
package.fetch("location"),
|
|
40
129
|
package.fetch("declarations").length,
|
|
41
130
|
*package.fetch("ruby_counts"),
|
|
42
|
-
|
|
131
|
+
system_index.fetch(old_index, -1),
|
|
43
132
|
]
|
|
44
133
|
end
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
package_order.each do |old_index|
|
|
134
|
+
|
|
135
|
+
Packages.new(
|
|
136
|
+
order: order,
|
|
137
|
+
rows: rows,
|
|
138
|
+
names: order.map { |old_index| snapshot_packages.fetch(old_index).fetch("name") },
|
|
139
|
+
morphologies: morphologies,
|
|
140
|
+
systems: systems,
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Dependency stars are emitted package by package in shuffled order, so a
|
|
145
|
+
# snapshot ordinal is only recoverable for the rows travel actually targets.
|
|
146
|
+
# Those rows are identified before the shuffle and tracked by object
|
|
147
|
+
# identity, since equal declaration rows are common and interchangeable.
|
|
148
|
+
#: (Hash[String, untyped], Packages, Namespaces, Random) -> Dependencies
|
|
149
|
+
def build_dependencies(snapshot, packages, namespaces, random)
|
|
150
|
+
targeted = travel_target_rows(snapshot, namespaces.referenced_dependency_ordinals)
|
|
151
|
+
rows = []
|
|
152
|
+
index = {}
|
|
153
|
+
packages.order.each_with_index do |old_index, new_index|
|
|
66
154
|
package = snapshot.fetch("packages").fetch(old_index)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
original_ordinal =
|
|
70
|
-
|
|
71
|
-
dependencies << [
|
|
72
|
-
random.rand(0..0xffff_ffff),
|
|
73
|
-
package_index.fetch(old_index),
|
|
74
|
-
*declaration.drop(1),
|
|
75
|
-
]
|
|
155
|
+
package.fetch("declarations").sort.shuffle(random: random).each do |declaration|
|
|
156
|
+
original_ordinal = targeted[declaration]
|
|
157
|
+
index[original_ordinal] = rows.length if original_ordinal
|
|
158
|
+
rows << [random.rand(0..0xffff_ffff), new_index, *declaration.drop(1)]
|
|
76
159
|
end
|
|
77
160
|
end
|
|
78
|
-
|
|
79
|
-
constant_reference_rows,
|
|
80
|
-
namespace_index,
|
|
81
|
-
dependency_index,
|
|
82
|
-
)
|
|
83
|
-
{
|
|
84
|
-
"schema" => "rubylens.art.v13",
|
|
85
|
-
"projectName" => snapshot.fetch("project_name"),
|
|
86
|
-
"morphology" => [morphology.fetch("family"), *morphology.fetch("knobs")],
|
|
87
|
-
"totals" => {
|
|
88
|
-
"namespaces" => namespaces.length,
|
|
89
|
-
"packages" => packages.length,
|
|
90
|
-
"dependencyStars" => dependencies.length,
|
|
91
|
-
},
|
|
92
|
-
"domains" => signal_domains(namespaces, snapshot.fetch("dependency_signal_maxima")),
|
|
93
|
-
"categoryStats" => snapshot.fetch("category_stats"),
|
|
94
|
-
"namespaceNames" => namespace_names,
|
|
95
|
-
"namespaces" => namespaces,
|
|
96
|
-
"constantReferenceLinks" => constant_reference_links,
|
|
97
|
-
"packageNames" => package_names,
|
|
98
|
-
"packages" => packages,
|
|
99
|
-
"packageMorphologies" => package_morphologies,
|
|
100
|
-
"dependencySystems" => dependency_systems,
|
|
101
|
-
"dependencyStars" => dependencies,
|
|
102
|
-
"dependencyWarnings" => snapshot.fetch("dependency_warnings", []).filter_map do |warning|
|
|
103
|
-
name = warning.fetch("name")
|
|
104
|
-
reason = warning.fetch("reason")
|
|
105
|
-
next unless name.is_a?(String) && DependencyWarning::NAME_PATTERN.match?(name)
|
|
106
|
-
next unless reason.is_a?(String) && DependencyWarning::ALLOWED_REASONS.include?(reason)
|
|
107
|
-
|
|
108
|
-
{ "name" => name, "reason" => reason }
|
|
109
|
-
end,
|
|
110
|
-
"warningCounts" => snapshot.fetch("warning_counts"),
|
|
111
|
-
}
|
|
161
|
+
Dependencies.new(rows: rows, index: index)
|
|
112
162
|
end
|
|
113
163
|
|
|
114
|
-
|
|
164
|
+
#: (Hash[String, untyped], Array[Integer]) -> Hash[Array[Integer], Integer]
|
|
165
|
+
def travel_target_rows(snapshot, dependency_ordinals)
|
|
166
|
+
offsets = []
|
|
167
|
+
total = 0
|
|
168
|
+
snapshot.fetch("packages").each do |package|
|
|
169
|
+
offsets << total
|
|
170
|
+
total += package.fetch("declarations").length
|
|
171
|
+
end
|
|
115
172
|
|
|
173
|
+
targeted = {}.compare_by_identity
|
|
174
|
+
dependency_ordinals.each do |ordinal|
|
|
175
|
+
next if ordinal.negative? || ordinal >= total
|
|
176
|
+
|
|
177
|
+
next_package = offsets.bsearch_index { |offset| offset > ordinal }
|
|
178
|
+
package_index = (next_package || offsets.length) - 1
|
|
179
|
+
declarations = snapshot.fetch("packages").fetch(package_index).fetch("declarations")
|
|
180
|
+
targeted[declarations.fetch(ordinal - offsets.fetch(package_index))] = ordinal
|
|
181
|
+
end
|
|
182
|
+
targeted
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
#: (Hash[String, untyped]) -> Array[Integer]
|
|
186
|
+
def morphology_row(morphology)
|
|
187
|
+
[morphology.fetch("family"), *morphology.fetch("knobs")]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Warnings reach the artifact only if they match the closed vocabulary, so a
|
|
191
|
+
# tampered snapshot cannot smuggle arbitrary text into a shareable page.
|
|
192
|
+
#: (Hash[String, untyped]) -> Array[Hash[String, String]]
|
|
193
|
+
def allowed_dependency_warnings(snapshot)
|
|
194
|
+
snapshot.fetch("dependency_warnings", []).filter_map do |warning|
|
|
195
|
+
name = warning.fetch("name")
|
|
196
|
+
reason = warning.fetch("reason")
|
|
197
|
+
next unless name.is_a?(String) && DependencyWarning::NAME_PATTERN.match?(name)
|
|
198
|
+
next unless reason.is_a?(String) && DependencyWarning::ALLOWED_REASONS.include?(reason)
|
|
199
|
+
|
|
200
|
+
{ "name" => name, "reason" => reason }
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
#: (Hash[String, untyped]) -> Array[[Integer, Integer]]
|
|
116
205
|
def valid_constant_reference_rows(snapshot)
|
|
117
206
|
rows = snapshot.fetch("constant_reference_links", [])
|
|
118
207
|
return [] unless rows.is_a?(Array)
|
|
@@ -122,6 +211,7 @@ module RubyLens
|
|
|
122
211
|
end
|
|
123
212
|
end
|
|
124
213
|
|
|
214
|
+
#: (Array[[Integer, Integer]], Array[Integer], Hash[Integer, Integer]) -> Array[[Integer, Integer]]
|
|
125
215
|
def build_constant_reference_links(rows, namespace_index, dependency_index)
|
|
126
216
|
snapshot_namespace_count = namespace_index.length
|
|
127
217
|
candidates = rows.filter_map do |row|
|
|
@@ -141,6 +231,7 @@ module RubyLens
|
|
|
141
231
|
candidates.shuffle(random: Random.new(@seed ^ 0xC057_A17E))
|
|
142
232
|
end
|
|
143
233
|
|
|
234
|
+
#: (Hash[String, untyped], Hash[Integer, Integer]) -> [Array[Array[Integer]], Hash[Integer, Integer]]
|
|
144
235
|
def build_dependency_systems(snapshot, package_index)
|
|
145
236
|
system_random = Random.new(@seed ^ 0xD3E5_157E)
|
|
146
237
|
package_system_index = {}
|
|
@@ -156,17 +247,16 @@ module RubyLens
|
|
|
156
247
|
[systems, package_system_index]
|
|
157
248
|
end
|
|
158
249
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
250
|
+
# Each signal is scaled against the larger of its workspace and dependency
|
|
251
|
+
# maxima, so the two populations stay comparable in the same scene.
|
|
252
|
+
#: (Array[Array[Integer]], Array[Integer]) -> Hash[String, Integer]
|
|
253
|
+
def signal_domains(namespace_rows, dependency_maxima)
|
|
254
|
+
namespace_domains = NAMESPACE_SIGNAL_COLUMNS.map do |column|
|
|
255
|
+
namespace_rows.map { |row| row[column].to_i }.max || 0
|
|
256
|
+
end
|
|
162
257
|
SIGNAL_FIELDS.each_with_index.to_h do |field, index|
|
|
163
258
|
[field, [namespace_domains[index], dependency_maxima[index]].max]
|
|
164
259
|
end
|
|
165
260
|
end
|
|
166
|
-
|
|
167
|
-
def maximum(rows, column)
|
|
168
|
-
rows.map { |row| row[column].to_i }.max || 0
|
|
169
|
-
end
|
|
170
|
-
|
|
171
261
|
end
|
|
172
262
|
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubydex"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module RubyLens
|
|
7
|
+
module Index
|
|
8
|
+
# Turns Rubydex's resolved constant references into two different things:
|
|
9
|
+
# the complete inbound reference count for every workspace namespace, and a
|
|
10
|
+
# bounded sample of directed links the renderer animates as travel.
|
|
11
|
+
#
|
|
12
|
+
# The two have deliberately different contracts. Counts are exact. Links
|
|
13
|
+
# stop at the limits below and are never ranked, so they are a visual
|
|
14
|
+
# sample and must not be read as a complete relationship or call graph.
|
|
15
|
+
class ConstantReferenceCollector
|
|
16
|
+
LINK_LIMIT = 1_024
|
|
17
|
+
ATTRIBUTION_LIMIT = LINK_LIMIT * 2
|
|
18
|
+
|
|
19
|
+
# `inbound_counts` is keyed by namespace ordinal, absent meaning zero;
|
|
20
|
+
# each link is [referring namespace ordinal, referenced endpoint ordinal].
|
|
21
|
+
Summary = Data.define(
|
|
22
|
+
:inbound_counts, #: Hash[Integer, Integer]
|
|
23
|
+
:links, #: Array[[Integer, Integer]]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
#: (locations: LocationIndex) -> void
|
|
27
|
+
def initialize(locations:)
|
|
28
|
+
@locations = locations
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `namespaces` are the collector's workspace namespaces in ordinal order;
|
|
32
|
+
# `ordinal_by_name` and `dependency_ordinal_by_name` address link
|
|
33
|
+
# endpoints, with dependency ordinals offset past the namespace block.
|
|
34
|
+
#
|
|
35
|
+
#: (graph: Rubydex::Graph, namespaces: Array[DeclarationCollector::Namespace], definition_ranges: Hash[String, Array[DeclarationCollector::definition_range]], ordinal_by_name: Hash[String, Integer], dependency_ordinal_by_name: Hash[String, Integer], namespace_count: Integer) -> Summary
|
|
36
|
+
def call(graph:, namespaces:, definition_ranges:, ordinal_by_name:, dependency_ordinal_by_name:, namespace_count:)
|
|
37
|
+
Summary.new(
|
|
38
|
+
inbound_counts: inbound_counts(namespaces),
|
|
39
|
+
links: links(graph, definition_ranges, ordinal_by_name, dependency_ordinal_by_name, namespace_count),
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Asking each namespace for its own references is bounded by the
|
|
46
|
+
# namespaces actually drawn, unlike scanning the global reference stream.
|
|
47
|
+
#
|
|
48
|
+
#: (Array[DeclarationCollector::Namespace]) -> Hash[Integer, Integer]
|
|
49
|
+
def inbound_counts(namespaces)
|
|
50
|
+
counts = Hash.new(0)
|
|
51
|
+
namespaces.each_with_index do |namespace, index|
|
|
52
|
+
namespace.declaration.references.each do |reference|
|
|
53
|
+
location = begin
|
|
54
|
+
reference.location
|
|
55
|
+
rescue StandardError
|
|
56
|
+
next
|
|
57
|
+
end
|
|
58
|
+
counts[index] += 1 if @locations.workspace?(location)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
counts.freeze
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#: (Rubydex::Graph, Hash[String, Array[DeclarationCollector::definition_range]], Hash[String, Integer], Hash[String, Integer], Integer) -> Array[[Integer, Integer]]
|
|
65
|
+
def links(graph, definition_ranges, ordinal_by_name, dependency_ordinal_by_name, namespace_count)
|
|
66
|
+
links = []
|
|
67
|
+
retained = Set.new
|
|
68
|
+
attributions = 0
|
|
69
|
+
|
|
70
|
+
graph.constant_references.each do |reference|
|
|
71
|
+
break if links.length >= LINK_LIMIT || attributions >= ATTRIBUTION_LIMIT
|
|
72
|
+
next unless reference.is_a?(Rubydex::ResolvedConstantReference)
|
|
73
|
+
|
|
74
|
+
name = referenced_name(reference)
|
|
75
|
+
next unless name
|
|
76
|
+
|
|
77
|
+
namespace_ordinal = ordinal_by_name[name]
|
|
78
|
+
dependency_ordinal = dependency_ordinal_by_name[name] unless namespace_ordinal
|
|
79
|
+
next unless namespace_ordinal || dependency_ordinal
|
|
80
|
+
|
|
81
|
+
location = begin
|
|
82
|
+
reference.location
|
|
83
|
+
rescue StandardError
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
next unless @locations.workspace?(location)
|
|
87
|
+
|
|
88
|
+
attributions += 1
|
|
89
|
+
endpoint = namespace_ordinal || namespace_count + dependency_ordinal
|
|
90
|
+
|
|
91
|
+
values = begin
|
|
92
|
+
location.comparable_values
|
|
93
|
+
rescue StandardError
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
origin = enclosing_namespace_ordinal(values, definition_ranges)
|
|
97
|
+
next unless origin
|
|
98
|
+
next if endpoint == origin
|
|
99
|
+
|
|
100
|
+
link = [origin, endpoint]
|
|
101
|
+
next unless retained.add?(link)
|
|
102
|
+
|
|
103
|
+
links << link
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
links.freeze
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
#: (Rubydex::ResolvedConstantReference) -> String?
|
|
110
|
+
def referenced_name(reference)
|
|
111
|
+
reference.declaration.name
|
|
112
|
+
rescue StandardError
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A reference belongs to the innermost namespace definition containing it.
|
|
117
|
+
# Two definitions sharing the exact same span cannot be told apart, so the
|
|
118
|
+
# reference is dropped rather than attributed to an arbitrary one.
|
|
119
|
+
#
|
|
120
|
+
#: (DeclarationCollector::site_key, Hash[String, Array[DeclarationCollector::definition_range]]) -> Integer?
|
|
121
|
+
def enclosing_namespace_ordinal(location_values, ranges_by_uri)
|
|
122
|
+
uri, start_line, start_column, end_line, end_column = location_values
|
|
123
|
+
best = nil
|
|
124
|
+
ambiguous = false
|
|
125
|
+
ranges_by_uri.fetch(uri, []).each do |range|
|
|
126
|
+
range_start_line, range_start_column, range_end_line, range_end_column, ordinal = range
|
|
127
|
+
next if compare(range_start_line, range_start_column, start_line, start_column).positive?
|
|
128
|
+
next if compare(range_end_line, range_end_column, end_line, end_column).negative?
|
|
129
|
+
|
|
130
|
+
start_comparison = best ? compare(range_start_line, range_start_column, best[0], best[1]) : 1
|
|
131
|
+
end_comparison = best && start_comparison.zero? ? compare(range_end_line, range_end_column, best[2], best[3]) : 0
|
|
132
|
+
if start_comparison.positive? || (start_comparison.zero? && end_comparison.negative?)
|
|
133
|
+
best = range
|
|
134
|
+
ambiguous = false
|
|
135
|
+
elsif start_comparison.zero? && end_comparison.zero? && ordinal != best[4]
|
|
136
|
+
ambiguous = true
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
ambiguous ? nil : best&.fetch(4)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
#: (Integer, Integer, Integer, Integer) -> Integer
|
|
144
|
+
def compare(left_line, left_column, right_line, right_column)
|
|
145
|
+
line_comparison = left_line <=> right_line
|
|
146
|
+
line_comparison.zero? ? left_column <=> right_column : line_comparison
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|