rubylens 0.1.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -39
  3. data/assets/runtime/report.js +1607 -251
  4. data/assets/shells/report.html +9 -7
  5. data/assets/shells/showcase.html +1 -0
  6. data/assets/styles/report.css +51 -25
  7. data/assets/styles/showcase.css +3 -0
  8. data/lib/rubylens/art_model_builder.rb +214 -72
  9. data/lib/rubylens/artifact_marker.rb +17 -0
  10. data/lib/rubylens/atomic_output.rb +27 -0
  11. data/lib/rubylens/cli.rb +53 -36
  12. data/lib/rubylens/clip/chrome_page.rb +183 -0
  13. data/lib/rubylens/clip/deadline_io.rb +28 -0
  14. data/lib/rubylens/clip/renderer.rb +113 -0
  15. data/lib/rubylens/clip/toolchain.rb +82 -0
  16. data/lib/rubylens/clip/web_socket_channel.rb +116 -0
  17. data/lib/rubylens/clip_generator.rb +102 -0
  18. data/lib/rubylens/default_output.rb +24 -0
  19. data/lib/rubylens/generator.rb +9 -10
  20. data/lib/rubylens/git_repository.rb +13 -7
  21. data/lib/rubylens/index/constant_reference_collector.rb +150 -0
  22. data/lib/rubylens/index/declaration_collector.rb +288 -0
  23. data/lib/rubylens/index/git_package_source.rb +202 -0
  24. data/lib/rubylens/index/location_index.rb +103 -0
  25. data/lib/rubylens/index/manifest.rb +62 -157
  26. data/lib/rubylens/index/rspec_extractor.rb +12 -10
  27. data/lib/rubylens/index/rubydex_adapter.rb +111 -301
  28. data/lib/rubylens/index/source_path.rb +12 -11
  29. data/lib/rubylens/model/dependency_aggregation.rb +11 -7
  30. data/lib/rubylens/morphology_classifier.rb +52 -12
  31. data/lib/rubylens/report_writer.rb +5 -20
  32. data/lib/rubylens/showcase_generator.rb +5 -9
  33. data/lib/rubylens/showcase_model.rb +12 -14
  34. data/lib/rubylens/showcase_writer.rb +0 -6
  35. data/lib/rubylens/version.rb +1 -1
  36. data/lib/rubylens.rb +7 -0
  37. metadata +16 -3
@@ -2,60 +2,75 @@
2
2
 
3
3
  require "rubydex"
4
4
  require "set"
5
- require_relative "../model/dependency_aggregation"
5
+ require_relative "constant_reference_collector"
6
+ require_relative "declaration_collector"
7
+ require_relative "location_index"
6
8
  require_relative "rspec_extractor"
7
- require_relative "source_path"
8
9
 
9
10
  module RubyLens
10
11
  module Index
12
+ # Drives one Rubydex indexing run over the manifest and assembles the
13
+ # snapshot payload from what the collectors return.
14
+ #
15
+ # The snapshot is the privacy boundary: every namespace and dependency star
16
+ # leaves here as a row of integers, and the only strings that survive are
17
+ # namespace names, package names, and the project name.
11
18
  class RubydexAdapter
12
- TEST_SEGMENTS = %w[test tests spec specs feature features].freeze
13
-
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
- )
19
+ SCHEMA = "rubylens.snapshot.v9"
20
+ RSPEC_ROW = [0, 1, *Array.new(11, 0)].freeze
23
21
 
22
+ # The manifest is duck-typed: production passes a Manifest, tests pass
23
+ # stubs, and `index` probes it with respond_to? for the optional parts.
24
+ #
25
+ #: (untyped manifest) -> void
24
26
  def initialize(manifest)
25
27
  @manifest = manifest
26
- @source_path_cache = {}
27
- @workspace_location_cache = {}
28
- @indexed_package_document_paths = Set.new
28
+ @locations = LocationIndex.new(manifest)
29
29
  end
30
30
 
31
+ #: () -> Hash[String, untyped]
31
32
  def index
32
33
  graph = Rubydex::Graph.new(workspace_path: @manifest.root.to_s)
33
34
  index_errors = graph.index_all(@manifest.files)
34
- @indexed_package_document_paths = indexed_package_document_paths(graph)
35
+ # Documents resolve before `resolve` so package attribution sees exactly
36
+ # the documents Rubydex accepted, not the paths the manifest offered.
37
+ documents_with_paths = @locations.resolve_documents(graph)
35
38
  graph.resolve
36
39
  integrity_failures = graph.check_integrity
37
- collected = collect_declarations(graph.declarations)
40
+
41
+ collected = DeclarationCollector.new(manifest: @manifest, locations: @locations).call(graph.declarations)
38
42
  rspec = RSpecExtractor.new(
39
43
  graph: graph,
40
44
  manifest: @manifest,
41
- package_document_paths: @indexed_package_document_paths,
45
+ package_document_paths: @locations.package_document_paths,
46
+ documents_with_paths: documents_with_paths,
42
47
  ).call
43
- workspace = workspace_namespaces(collected.fetch(:workspace_records), rspec.groups)
44
- inbound_references = inbound_workspace_references(graph, workspace.fetch(:ordinal_by_name))
45
- category_stats = collected.fetch(:category_stats)
48
+
49
+ namespaces = collected.namespaces
50
+ namespace_names = namespaces.map(&:name) + rspec.groups
51
+ references = ConstantReferenceCollector.new(locations: @locations).call(
52
+ graph: graph,
53
+ namespaces: namespaces,
54
+ definition_ranges: collected.definition_ranges,
55
+ ordinal_by_name: ordinal_by_name(namespaces),
56
+ dependency_ordinal_by_name: collected.dependency_ordinal_by_name,
57
+ namespace_count: namespace_names.length,
58
+ )
59
+
60
+ category_stats = collected.category_stats
46
61
  category_stats.fetch("tests")[0] += rspec.groups.length
47
62
  category_stats.fetch("tests")[2] += rspec.method_count
48
63
 
49
64
  {
50
- "schema" => "rubylens.snapshot.v6",
65
+ "schema" => SCHEMA,
51
66
  "project_name" => project_name,
52
- "components" => workspace.fetch(:component_counts),
53
- "namespace_names" => workspace.fetch(:namespace_names),
54
- "namespaces" => build_workspace_rows(workspace, inbound_references),
67
+ "namespace_names" => namespace_names,
68
+ "namespaces" => namespace_rows(namespaces, references.inbound_counts, rspec.groups.length),
69
+ "constant_reference_links" => references.links,
55
70
  "category_stats" => category_stats,
56
- "dependency_signal_maxima" => collected.fetch(:dependency_aggregation).signal_maxima,
57
- "packages" => build_package_rows(collected.fetch(:dependency_aggregation)),
58
- "dependency_systems" => build_dependency_system_rows,
71
+ "dependency_signal_maxima" => collected.dependency_signal_maxima,
72
+ "packages" => package_rows(collected.dependency_packages),
73
+ "dependency_systems" => dependency_system_rows,
59
74
  "dependency_warnings" => @manifest.respond_to?(:dependency_warnings) ? @manifest.dependency_warnings : [],
60
75
  "warning_counts" => {
61
76
  "manifest" => @manifest.warnings.length,
@@ -67,130 +82,100 @@ module RubyLens
67
82
 
68
83
  private
69
84
 
70
- def collect_declarations(declarations)
71
- records = []
72
- category_stats = { "core" => Array.new(4, 0), "tests" => Array.new(4, 0) }
73
- aggregation = Model::DependencyAggregation.new(package_count: @manifest.packages.length)
74
-
75
- 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
- ]
86
- end
87
- collect_category_stat(category_stats, declaration, summary)
88
- collect_dependency_declaration(aggregation, declaration, summary)
89
- end
90
-
91
- { workspace_records: records, category_stats:, dependency_aggregation: aggregation }
92
- end
93
-
94
- def summarize_definitions(declaration)
95
- namespace = namespace?(declaration)
96
- workspace_count = 0
97
- workspace_relatives = []
98
- canonical_relatives = []
99
- canonical_site_keys = []
100
- package_site_keys = {}
101
-
102
- declaration.definitions.each do |definition|
103
- location = definition.location
104
- if workspace_location?(location)
105
- 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
111
- end
112
- end
113
- package_index = package_index_for_location(location)
114
- (package_site_keys[package_index] ||= []) << site_key(location) if package_index
115
- end
116
-
117
- DefinitionSummary.new(
118
- workspace_count:,
119
- workspace_relatives:,
120
- canonical_relatives:,
121
- canonical_site_keys:,
122
- package_site_keys:,
123
- )
124
- end
125
-
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
-
132
- {
133
- records: records,
134
- rspec_groups: rspec_groups,
135
- namespace_names: records.map { |declaration, *| declaration.name } + rspec_groups.map(&:name),
136
- 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
- }
141
- end
142
-
143
- def build_workspace_rows(workspace, inbound_references)
144
- rows = workspace.fetch(:records).each_with_index.map do |(declaration, sites, scope, component), index|
85
+ #: (Array[DeclarationCollector::Namespace]) -> Hash[String, Integer]
86
+ def ordinal_by_name(namespaces)
87
+ ordinals = {}
88
+ namespaces.each_with_index { |namespace, index| ordinals[namespace.name] = index }
89
+ ordinals
90
+ end
91
+
92
+ #: (Array[DeclarationCollector::Namespace], Hash[Integer, Integer], Integer) -> Array[Array[Integer]]
93
+ def namespace_rows(namespaces, inbound_counts, rspec_group_count)
94
+ drawn_names = ordinal_by_name(namespaces)
95
+ rows = namespaces.each_with_index.map do |namespace, index|
96
+ declaration = namespace.declaration
97
+ sites = namespace.definition_sites
98
+ # A namespace only earns a row through a class or module definition,
99
+ # so its own construct is exactly this kind.
100
+ kind = declaration.is_a?(Rubydex::Class) ? DeclarationCollector::CLASS : DeclarationCollector::MODULE
145
101
  descendants = declaration.descendants.count do |descendant|
146
- descendant.name != declaration.name && workspace.fetch(:ordinal_by_name).key?(descendant.name)
102
+ name = descendant.name
103
+ name != namespace.name && drawn_names.key?(name)
147
104
  end
148
- member_count, ruby_counts, instance_variable_count = member_statistics(
105
+ members, ruby_counts, instance_variables = member_statistics(
149
106
  declaration,
150
- count_instance_variables: declaration.is_a?(Rubydex::Class) && scope != 1,
107
+ kind: kind,
108
+ # Instance variables read as state on a class; test classes carry
109
+ # fixture state that would misrepresent them next to core classes.
110
+ count_instance_variables: kind == DeclarationCollector::CLASS && namespace.scope != LocationIndex::TEST,
151
111
  )
152
112
  [
153
- workspace.fetch(:component_ids).fetch(component),
154
- declaration.is_a?(Rubydex::Class) ? 0 : 1,
155
- scope,
113
+ kind,
114
+ namespace.scope,
156
115
  [declaration.ancestors.count - 1, 0].max,
157
116
  sites,
158
117
  [sites - 1, 0].max,
159
118
  descendants,
160
- inbound_references.fetch(index, 0),
161
- member_count,
119
+ inbound_counts.fetch(index, 0),
120
+ members,
162
121
  *ruby_counts,
163
- instance_variable_count,
122
+ instance_variables,
164
123
  ]
165
124
  end
166
- rows.concat(workspace.fetch(:rspec_groups).map { |group| build_rspec_row(group, workspace) })
125
+ rows.concat(Array.new(rspec_group_count) { RSPEC_ROW.dup })
167
126
  end
168
127
 
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
- ]
128
+ # One sweep over a namespace's direct and singleton members yields the
129
+ # workspace member count, the method and constant construct counts, and
130
+ # the instance-variable count together, since each of the three otherwise
131
+ # re-reads every member's definitions.
132
+ #
133
+ #: (Rubydex::Namespace, kind: Integer, count_instance_variables: bool) -> [Integer, Array[Integer], Integer]
134
+ def member_statistics(declaration, kind:, count_instance_variables:)
135
+ members = 0
136
+ instance_variables = 0
137
+ ruby_counts = Array.new(4, 0)
138
+ ruby_counts[kind] += 1
139
+
140
+ seen = Set.new
141
+ groups = [[declaration.members, true]]
142
+ singleton = declaration.singleton_class
143
+ groups << [singleton.members, false] if singleton
144
+ groups.each do |group, direct|
145
+ group.each do |member|
146
+ next unless seen.add?(member.name)
147
+ next unless member.definitions.any? { |definition| @locations.workspace?(definition.location) }
148
+
149
+ members += 1
150
+ construct = case member
151
+ when Rubydex::Method then DeclarationCollector::METHOD
152
+ when Rubydex::Constant, Rubydex::ConstantAlias then DeclarationCollector::CONSTANT
153
+ end
154
+ ruby_counts[construct] += 1 if construct
155
+ instance_variables += 1 if direct && count_instance_variables && member.is_a?(Rubydex::InstanceVariable)
156
+ end
157
+ end
158
+ [members, ruby_counts, instance_variables]
159
+ rescue StandardError
160
+ [members, ruby_counts, instance_variables]
176
161
  end
177
162
 
178
- def build_package_rows(aggregation)
179
- aggregates = aggregation.packages
163
+ #: (Array[DeclarationCollector::dependency_package]) -> Array[Hash[String, untyped]]
164
+ def package_rows(aggregates)
180
165
  @manifest.packages.each_with_index.map do |package, index|
181
166
  aggregate = aggregates.fetch(index)
182
167
  {
183
168
  "name" => package.name,
184
169
  "role" => package.role == "direct" ? 0 : 1,
185
170
  "location" => package.location == "workspace" ? 0 : 1,
186
- "declaration_count" => aggregate.fetch(:declaration_count),
187
171
  "ruby_counts" => aggregate.fetch(:ruby_counts),
188
172
  "declarations" => aggregate.fetch(:declarations),
189
173
  }
190
174
  end
191
175
  end
192
176
 
193
- def build_dependency_system_rows
177
+ #: () -> Array[Hash[String, untyped]]
178
+ def dependency_system_rows
194
179
  return [] unless @manifest.respond_to?(:dependency_systems)
195
180
 
196
181
  @manifest.dependency_systems.map do |system|
@@ -202,182 +187,7 @@ module RubyLens
202
187
  end
203
188
  end
204
189
 
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]
208
- end
209
- return unless package_index
210
-
211
- sites = site_keys.uniq.length
212
- row = [
213
- namespace_kind(declaration),
214
- namespace?(declaration) ? [declaration.ancestors.count - 1, 0].max : 0,
215
- sites,
216
- [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(
222
- package_index:,
223
- row:,
224
- construct_index: ruby_construct_index(declaration),
225
- )
226
- end
227
-
228
- def inbound_workspace_references(graph, ordinal_by_name)
229
- graph.constant_references.each_with_object(Hash.new(0)) do |reference, counts|
230
- next unless reference.is_a?(Rubydex::ResolvedConstantReference)
231
-
232
- ordinal = ordinal_by_name[reference.declaration.name]
233
- next unless ordinal
234
-
235
- counts[ordinal] += 1 if workspace_location?(reference.location)
236
- rescue StandardError
237
- next
238
- end
239
- end
240
-
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)
245
-
246
- @manifest.package_index_for(path)
247
- end
248
-
249
- def indexed_package_document_paths(graph)
250
- audited = @manifest.packages.flat_map(&:files).to_set
251
- graph.documents.each_with_object(Set.new) do |document, paths|
252
- path = source_path(document.uri)
253
- paths << path if path && audited.include?(path)
254
- end
255
- end
256
-
257
- # One sweep over a namespace's direct and singleton members yields the
258
- # workspace member count, the method/constant construct counts, and the
259
- # instance-variable count that previously took three walks each re-reading
260
- # every member's definitions.
261
- def member_statistics(declaration, count_instance_variables:)
262
- member_count = 0
263
- instance_variable_count = 0
264
- ruby_counts = Array.new(4, 0)
265
- own_construct = ruby_construct_index(declaration)
266
- ruby_counts[own_construct] += 1 if own_construct == 0 || own_construct == 1
267
-
268
- seen_names = Set.new
269
- walk = lambda do |members, direct|
270
- members.each do |member|
271
- next unless seen_names.add?(member.name)
272
- next unless member.definitions.any? { |definition| workspace_location?(definition.location) }
273
-
274
- member_count += 1
275
- construct_index = ruby_construct_index(member)
276
- ruby_counts[construct_index] += 1 if construct_index && construct_index >= 2
277
- if direct && count_instance_variables && member.is_a?(Rubydex::InstanceVariable)
278
- instance_variable_count += 1
279
- end
280
- end
281
- end
282
- walk.call(declaration.members, true)
283
- walk.call(declaration.singleton_class.members, false) if declaration.singleton_class
284
- [member_count, ruby_counts, instance_variable_count]
285
- rescue StandardError
286
- [member_count, ruby_counts, instance_variable_count]
287
- end
288
-
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
- def workspace_location?(location)
299
- 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
304
- rescue StandardError
305
- false
306
- end
307
-
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
322
- end
323
-
324
- def namespace?(declaration)
325
- declaration.is_a?(Rubydex::Namespace)
326
- end
327
-
328
- def model_eligible_declaration?(declaration)
329
- name = declaration.name
330
- return false if name.nil? || name.empty? || name.include?("<anonymous>")
331
- return false if declaration.is_a?(Rubydex::Todo)
332
- return true unless declaration.is_a?(Rubydex::SingletonClass)
333
-
334
- !declaration.attached_class.is_a?(Rubydex::SingletonClass) &&
335
- !declaration.attached_class.is_a?(Rubydex::Todo)
336
- end
337
-
338
- def canonical_namespace_definition?(declaration, definition)
339
- (declaration.is_a?(Rubydex::Class) && definition.is_a?(Rubydex::ClassDefinition)) ||
340
- (declaration.is_a?(Rubydex::Module) && definition.is_a?(Rubydex::ModuleDefinition))
341
- end
342
-
343
- def namespace_kind(declaration)
344
- case declaration
345
- when Rubydex::Class then 0
346
- when Rubydex::Module then 1
347
- else 2
348
- end
349
- end
350
-
351
- def ruby_construct_index(declaration)
352
- case declaration
353
- when Rubydex::SingletonClass then nil
354
- when Rubydex::Class then 0
355
- when Rubydex::Module then 1
356
- when Rubydex::Method then 2
357
- when Rubydex::Constant, Rubydex::ConstantAlias then 3
358
- end
359
- end
360
-
361
- 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)
365
- end
366
-
367
- def safe_length(object, method)
368
- records = object.public_send(method)
369
- size = records.size
370
- size.nil? ? safe_count(records) : size
371
- rescue StandardError
372
- safe_count(records)
373
- end
374
-
375
- def safe_count(records)
376
- records ? records.count : 0
377
- rescue StandardError
378
- 0
379
- end
380
-
190
+ #: () -> String
381
191
  def project_name
382
192
  basename = @manifest.root.basename.to_s
383
193
  return "IRB" if basename.casecmp("irb").zero?
@@ -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
@@ -8,25 +8,29 @@ module RubyLens
8
8
  def initialize(package_count:)
9
9
  raise ArgumentError, "package_count must be nonnegative" if package_count.negative?
10
10
 
11
- @counts = Array.new(package_count, 0)
12
11
  @ruby_counts = Array.new(package_count) { Array.new(4, 0) }
13
12
  @signal_maxima = Array.new(6, 0)
14
13
  @rows = Array.new(package_count) { [] }
15
14
  end
16
15
 
17
16
  def add(package_index:, row:, construct_index:)
18
- @counts[package_index] += 1
17
+ local_index = @rows[package_index].length
19
18
  @ruby_counts[package_index][construct_index] += 1 if construct_index
20
- SIGNAL_COLUMNS.each_with_index do |column, index|
21
- @signal_maxima[index] = [@signal_maxima[index], row[column]].max
19
+ maxima = @signal_maxima
20
+ column = SIGNAL_COLUMNS.begin
21
+ while column <= SIGNAL_COLUMNS.end
22
+ value = row[column]
23
+ index = column - 1
24
+ maxima[index] = value if value > maxima[index]
25
+ column += 1
22
26
  end
23
- @rows[package_index] << row.dup.freeze
27
+ @rows[package_index] << (row.frozen? ? row : row.dup.freeze)
28
+ local_index
24
29
  end
25
30
 
26
31
  def packages
27
- @counts.each_index.map do |index|
32
+ @rows.each_index.map do |index|
28
33
  {
29
- declaration_count: @counts[index],
30
34
  ruby_counts: @ruby_counts[index].dup.freeze,
31
35
  declarations: @rows[index].dup.freeze,
32
36
  }.freeze