rubylens 0.1.0.pre.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.
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+ require "bundler/lockfile_parser"
5
+ require "find"
6
+ require "pathname"
7
+ require "set"
8
+
9
+ module RubyLens
10
+ module Index
11
+ class Manifest
12
+ Package = Data.define(:name, :version, :role, :location, :root, :files)
13
+
14
+ attr_reader :root, :files, :workspace_files, :packages, :warnings
15
+
16
+ def self.build(root:, lockfile: nil)
17
+ new(root: root, lockfile: lockfile).tap(&:build)
18
+ end
19
+
20
+ def initialize(root:, lockfile: nil)
21
+ @root = Pathname(root).expand_path.realpath
22
+ @lockfile = Pathname(lockfile || @root.join("Gemfile.lock")).expand_path
23
+ @warnings = []
24
+ @packages = []
25
+ @package_roots = []
26
+ @package_index_by_file = {}
27
+ @package_index_cache = {}
28
+ @relative_workspace_path_cache = {}
29
+ end
30
+
31
+ def build
32
+ @workspace_files = GitRepository.new(@root).selected_files.freeze
33
+ build_packages
34
+ @package_roots = @packages.each_with_index.map { |package, index| [package.root, index] }
35
+ .sort_by { |package_root, _index| -package_root.to_s.length }
36
+ build_package_index
37
+ @files = (@workspace_files + @packages.flat_map(&:files)).uniq.freeze
38
+ self
39
+ end
40
+
41
+ def package_index_for(path)
42
+ path = path.to_s
43
+ return @package_index_by_file[path] if @package_index_by_file.key?(path)
44
+ return @package_index_cache[path] if @package_index_cache.key?(path)
45
+
46
+ @package_index_cache[path] = uncached_package_index_for(path)
47
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
48
+ nil
49
+ end
50
+
51
+ def workspace_path?(path)
52
+ Paths.inside?(path, @root)
53
+ end
54
+
55
+ def relative_workspace_path(path)
56
+ path = path.to_s
57
+ return @relative_workspace_path_cache[path] if @relative_workspace_path_cache.key?(path)
58
+
59
+ @relative_workspace_path_cache[path] = Pathname(path).realpath.relative_path_from(@root).to_s
60
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP, ArgumentError
61
+ nil
62
+ end
63
+
64
+ private
65
+
66
+ def build_package_index
67
+ package_index_by_file = {}
68
+ @package_roots.each do |_package_root, index|
69
+ @packages.fetch(index).files.each do |path|
70
+ path = path.to_s
71
+ package_index_by_file[path] = index unless package_index_by_file.key?(path)
72
+ end
73
+ end
74
+ @workspace_files.each do |path|
75
+ path = path.to_s
76
+ package_index_by_file[path] = nil unless package_index_by_file.key?(path)
77
+ end
78
+ @package_index_by_file = package_index_by_file.freeze
79
+ end
80
+
81
+ def uncached_package_index_for(path)
82
+ resolved = Pathname(path).expand_path
83
+ resolved = resolved.realpath if resolved.exist?
84
+ resolved_path = resolved.to_s
85
+ return @package_index_by_file[resolved_path] if @package_index_by_file.key?(resolved_path)
86
+
87
+ entry = @package_roots.find { |package_root, _index| Paths.inside?(resolved, package_root) }
88
+ entry&.last
89
+ end
90
+
91
+ def build_packages
92
+ unless @lockfile.file?
93
+ @warnings << "No Gemfile.lock found; dependency systems were omitted."
94
+ return
95
+ end
96
+
97
+ parser = Bundler::LockfileParser.new(@lockfile.read)
98
+ direct_names = parser.dependencies.keys.to_set
99
+ excluded_names = tool_only_dependency_names(parser, direct_names)
100
+ parser.specs.uniq { |specification| [specification.name, specification.version.to_s] }.each do |locked|
101
+ next if excluded_names.include?(locked.name)
102
+
103
+ package = package_for(locked, direct_names)
104
+ @packages << package if package
105
+ end
106
+ rescue Bundler::LockfileError, Errno::EACCES => error
107
+ @warnings << "Gemfile.lock could not be read: #{error.class}."
108
+ end
109
+
110
+ def package_for(locked, direct_names)
111
+ source = locked.source.class.name.split("::").last.downcase
112
+ role = direct_names.include?(locked.name) ? "direct" : "transitive"
113
+ if source == "path"
114
+ return local_package(locked)
115
+ end
116
+ unless source == "rubygems"
117
+ @warnings << "Skipped #{locked.name} #{locked.version}: #{source} dependencies are not indexed yet."
118
+ return nil
119
+ end
120
+
121
+ specification = installed_specification(locked)
122
+ unless specification
123
+ @warnings << "Skipped #{locked.name} #{locked.version}: exact installed gem not found."
124
+ return nil
125
+ end
126
+
127
+ root = Pathname(specification.full_gem_path).realpath
128
+ files = indexable_files(root, specification.require_paths)
129
+ Package.new(
130
+ name: locked.name,
131
+ version: locked.version.to_s,
132
+ role: role,
133
+ location: "external",
134
+ root: root,
135
+ files: files.freeze,
136
+ )
137
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP => error
138
+ @warnings << "Skipped #{locked.name} #{locked.version}: #{error.class}."
139
+ nil
140
+ end
141
+
142
+ def local_package(locked)
143
+ source_path = locked.source.respond_to?(:path) ? locked.source.path : nil
144
+ unless source_path
145
+ @warnings << "Skipped #{locked.name} #{locked.version}: local source path is unavailable."
146
+ return nil
147
+ end
148
+
149
+ root = Pathname(source_path.to_s)
150
+ root = @lockfile.dirname.join(root) unless root.absolute?
151
+ root = root.realpath
152
+ unless Paths.inside?(root, @root)
153
+ @warnings << "Skipped #{locked.name} #{locked.version}: local dependency is outside the project."
154
+ end
155
+ nil
156
+ end
157
+
158
+ def tool_only_dependency_names(parser, direct_names)
159
+ return Set.new unless direct_names.include?("rubylens")
160
+
161
+ dependencies = parser.specs.each_with_object(Hash.new { |hash, name| hash[name] = Set.new }) do |specification, graph|
162
+ specification.dependencies.each { |dependency| graph[specification.name] << dependency.name }
163
+ end
164
+ tool_reach = dependency_reach("rubylens", dependencies)
165
+ project_reach = (direct_names - ["rubylens"]).each_with_object(Set.new) do |root, names|
166
+ names.merge(dependency_reach(root, dependencies))
167
+ end
168
+ tool_reach - project_reach
169
+ end
170
+
171
+ def dependency_reach(root, dependencies)
172
+ visited = Set.new
173
+ pending = [root]
174
+ until pending.empty?
175
+ name = pending.pop
176
+ next unless visited.add?(name)
177
+
178
+ pending.concat(dependencies.fetch(name, []).to_a)
179
+ end
180
+ visited
181
+ end
182
+
183
+ def installed_specification(locked)
184
+ candidates = Gem::Specification.find_all_by_name(locked.name, "= #{locked.version}")
185
+ platform = locked.platform.to_s
186
+ candidates.find { |candidate| candidate.platform.to_s == platform } ||
187
+ candidates.find { |candidate| candidate.platform.to_s == Gem::Platform.local.to_s } ||
188
+ candidates.find { |candidate| candidate.platform.to_s == "ruby" } ||
189
+ candidates.first
190
+ end
191
+
192
+ def indexable_files(root, require_paths)
193
+ require_paths.filter_map do |relative_path|
194
+ next if File.absolute_path?(relative_path)
195
+
196
+ candidate = root.join(relative_path)
197
+ next unless candidate.exist?
198
+
199
+ resolved = candidate.realpath
200
+ next unless Paths.inside?(resolved, root)
201
+
202
+ resolved
203
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
204
+ nil
205
+ end.flat_map { |path| enumerate(path, root) }.uniq.sort
206
+ end
207
+
208
+ def enumerate(path, root)
209
+ return [path.to_s] if path.file? && indexable?(path) && Paths.inside?(path.realpath, root)
210
+ return [] unless path.directory?
211
+
212
+ files = []
213
+ Find.find(path) do |candidate|
214
+ candidate = Pathname(candidate)
215
+ next if candidate.directory?
216
+ next unless indexable?(candidate)
217
+
218
+ resolved = candidate.realpath
219
+ files << resolved.to_s if Paths.inside?(resolved, root)
220
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
221
+ next
222
+ end
223
+ files
224
+ end
225
+
226
+ def indexable?(path)
227
+ GitRepository::INDEXABLE_EXTENSIONS.include?(path.extname)
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubydex"
4
+ require "uri"
5
+ require_relative "../model/dependency_aggregation"
6
+
7
+ module RubyLens
8
+ module Index
9
+ class RubydexAdapter
10
+ TEST_SEGMENTS = %w[test tests spec specs feature features].freeze
11
+
12
+ def initialize(graph_factory: nil)
13
+ @graph_factory = graph_factory || ->(root) { Rubydex::Graph.new(workspace_path: root.to_s) }
14
+ end
15
+
16
+ def index(manifest)
17
+ @location_path_cache = {}
18
+ @workspace_location_cache = {}
19
+ graph = @graph_factory.call(manifest.root)
20
+ index_errors = Array(graph.index_all(manifest.files))
21
+ graph.resolve
22
+ integrity_failures = Array(graph.check_integrity)
23
+ collected = collect_declarations(graph.declarations, manifest)
24
+ workspace = workspace_namespaces(collected.fetch(:workspace_records), manifest)
25
+ inbound_references = inbound_workspace_references(graph, manifest, workspace.fetch(:ordinal_by_name))
26
+
27
+ {
28
+ "schema" => "rubylens.snapshot.v5",
29
+ "project_name" => project_name(manifest),
30
+ "components" => workspace.fetch(:component_counts),
31
+ "namespace_names" => workspace.fetch(:records).map { |declaration, _definitions| declaration.name },
32
+ "namespaces" => build_workspace_rows(workspace, inbound_references, manifest),
33
+ "category_stats" => collected.fetch(:category_stats),
34
+ "dependency_signal_maxima" => collected.fetch(:dependency_aggregation).signal_maxima,
35
+ "packages" => build_package_rows(collected.fetch(:dependency_aggregation), manifest),
36
+ "warning_counts" => {
37
+ "manifest" => manifest.warnings.length,
38
+ "index" => index_errors.length,
39
+ "integrity" => integrity_failures.length,
40
+ },
41
+ }
42
+ ensure
43
+ @location_path_cache = nil
44
+ @workspace_location_cache = nil
45
+ end
46
+
47
+ private
48
+
49
+ def collect_declarations(declarations, manifest)
50
+ records = []
51
+ category_stats = { "core" => Array.new(4, 0), "tests" => Array.new(4, 0) }
52
+ aggregation = Model::DependencyAggregation.new(package_count: manifest.packages.length)
53
+
54
+ declarations.each do |declaration|
55
+ if namespace?(declaration)
56
+ definitions = declaration.definitions.select do |definition|
57
+ canonical_namespace_definition?(declaration, definition) && workspace_location?(definition.location, manifest)
58
+ end
59
+ records << [declaration, definitions] unless definitions.empty?
60
+ end
61
+ collect_category_stat(category_stats, declaration, manifest)
62
+ collect_dependency_declaration(aggregation, declaration, manifest)
63
+ end
64
+
65
+ { workspace_records: records, category_stats:, dependency_aggregation: aggregation }
66
+ end
67
+
68
+ def workspace_namespaces(records, manifest)
69
+ ordinal_by_name = records.each_with_index.to_h { |(declaration, _definitions), index| [declaration.name, index] }
70
+ components = records.map { |_declaration, definitions| component_for(definitions, manifest) }
71
+ component_ids = components.uniq.sort.each_with_index.to_h
72
+
73
+ {
74
+ records: records,
75
+ ordinal_by_name: ordinal_by_name,
76
+ component_ids: component_ids,
77
+ components: components,
78
+ component_counts: components.tally.sort_by { |name, _count| component_ids.fetch(name) }.map(&:last),
79
+ }
80
+ end
81
+
82
+ def build_workspace_rows(workspace, inbound_references, manifest)
83
+ workspace.fetch(:records).each_with_index.map do |(declaration, definitions), index|
84
+ sites = definitions.map { |definition| site_key(definition.location) }.uniq.length
85
+ scope = scope_for(definitions, manifest)
86
+ descendants = declaration.descendants.count do |descendant|
87
+ descendant.name != declaration.name && workspace.fetch(:ordinal_by_name).key?(descendant.name)
88
+ end
89
+ [
90
+ workspace.fetch(:component_ids).fetch(workspace.fetch(:components)[index]),
91
+ declaration.is_a?(Rubydex::Class) ? 0 : 1,
92
+ scope,
93
+ [declaration.ancestors.count - 1, 0].max,
94
+ sites,
95
+ [sites - 1, 0].max,
96
+ descendants,
97
+ inbound_references.fetch(index, 0),
98
+ workspace_member_count(declaration, manifest),
99
+ *namespace_ruby_counts(declaration, manifest),
100
+ namespace_instance_variable_count(declaration, manifest, scope),
101
+ ]
102
+ end
103
+ end
104
+
105
+ def build_package_rows(aggregation, manifest)
106
+ aggregates = aggregation.packages
107
+ manifest.packages.each_with_index.map do |package, index|
108
+ aggregate = aggregates.fetch(index)
109
+ {
110
+ "name" => package.name,
111
+ "role" => package.role == "direct" ? 0 : 1,
112
+ "location" => package.location == "workspace" ? 0 : 1,
113
+ "declaration_count" => aggregate.fetch(:declaration_count),
114
+ "ruby_counts" => aggregate.fetch(:ruby_counts),
115
+ "declarations" => aggregate.fetch(:declarations),
116
+ }
117
+ end
118
+ end
119
+
120
+ def collect_dependency_declaration(aggregation, declaration, manifest)
121
+ package_index, definitions = dominant_package_definitions(declaration, manifest)
122
+ return unless package_index
123
+
124
+ sites = definitions.map { |definition| site_key(definition.location) }.uniq.length
125
+ row = [
126
+ namespace_kind(declaration),
127
+ namespace?(declaration) ? [declaration.ancestors.count - 1, 0].max : 0,
128
+ sites,
129
+ [sites - 1, 0].max,
130
+ namespace?(declaration) ? [declaration.descendants.count - 1, 0].max : 0,
131
+ safe_length(declaration, :references),
132
+ namespace?(declaration) ? safe_length(declaration, :members) : 0,
133
+ ]
134
+ aggregation.add(package_index:, row:, construct_index: ruby_construct_index(declaration))
135
+ end
136
+
137
+ def inbound_workspace_references(graph, manifest, ordinal_by_name)
138
+ graph.constant_references.each_with_object(Hash.new(0)) do |reference, counts|
139
+ next unless reference.respond_to?(:declaration)
140
+
141
+ next unless workspace_location?(reference.location, manifest)
142
+
143
+ target = reference.declaration
144
+ ordinal = ordinal_by_name[target.name]
145
+ counts[ordinal] += 1 if ordinal
146
+ rescue StandardError
147
+ next
148
+ end
149
+ end
150
+
151
+ def dominant_package_definitions(declaration, manifest)
152
+ grouped = declaration.definitions.group_by do |definition|
153
+ package_index_for_location(definition.location, manifest)
154
+ end
155
+ grouped.reject { |index, _records| index.nil? }
156
+ .max_by { |index, records| [records.length, -index] }
157
+ end
158
+
159
+ def package_index_for_location(location, manifest)
160
+ uri_string = location.uri
161
+ return nil unless URI.parse(uri_string).scheme == "file"
162
+
163
+ manifest.package_index_for(location_path(location, uri_string))
164
+ end
165
+
166
+ def workspace_member_count(declaration, manifest)
167
+ members = declaration.members.to_a
168
+ members.concat(declaration.singleton_class.members.to_a) if declaration.singleton_class
169
+ members.uniq(&:name).count do |member|
170
+ member.definitions.any? { |definition| workspace_location?(definition.location, manifest) }
171
+ end
172
+ rescue StandardError
173
+ 0
174
+ end
175
+
176
+ def namespace_ruby_counts(declaration, manifest)
177
+ counts = Array.new(4, 0)
178
+ own_construct = ruby_construct_index(declaration)
179
+ counts[own_construct] += 1 if own_construct == 0 || own_construct == 1
180
+
181
+ members = declaration.members.to_a
182
+ members.concat(declaration.singleton_class.members.to_a) if declaration.singleton_class
183
+ members.uniq(&:name).each do |member|
184
+ construct_index = ruby_construct_index(member)
185
+ next unless construct_index && construct_index >= 2
186
+ next unless member.definitions.any? { |definition| workspace_location?(definition.location, manifest) }
187
+
188
+ counts[construct_index] += 1
189
+ end
190
+ counts
191
+ rescue StandardError
192
+ counts || Array.new(4, 0)
193
+ end
194
+
195
+ def namespace_instance_variable_count(declaration, manifest, scope)
196
+ return 0 unless declaration.is_a?(Rubydex::Class) && scope != 1
197
+
198
+ declaration.members.to_a.uniq(&:name).count do |member|
199
+ member.is_a?(Rubydex::InstanceVariable) &&
200
+ member.definitions.any? { |definition| workspace_location?(definition.location, manifest) }
201
+ end
202
+ rescue StandardError
203
+ 0
204
+ end
205
+
206
+ def collect_category_stat(stats, declaration, manifest)
207
+ construct_index = ruby_construct_index(declaration)
208
+ return unless construct_index
209
+
210
+ definitions = declaration.definitions.select do |definition|
211
+ workspace_location?(definition.location, manifest)
212
+ end
213
+ return if definitions.empty?
214
+
215
+ category = scope_for(definitions, manifest) == 1 ? "tests" : "core"
216
+ stats.fetch(category)[construct_index] += 1
217
+ rescue StandardError
218
+ nil
219
+ end
220
+
221
+ def workspace_location?(location, manifest)
222
+ uri = location.uri
223
+ @workspace_location_cache ||= {}
224
+ return @workspace_location_cache[uri] if @workspace_location_cache.key?(uri)
225
+
226
+ @workspace_location_cache[uri] = manifest.workspace_path?(location_path(location, uri))
227
+ rescue StandardError
228
+ false
229
+ end
230
+
231
+ def component_for(definitions, manifest)
232
+ candidates = definitions.filter_map do |definition|
233
+ relative = manifest.relative_workspace_path(location_path(definition.location))
234
+ next unless relative
235
+
236
+ segments = relative.split(File::SEPARATOR)
237
+ first = segments.first || "root"
238
+ if %w[lib app test tests spec specs].include?(first)
239
+ "#{first}/#{segments[1] || "root"}"
240
+ else
241
+ first
242
+ end
243
+ end
244
+ candidates.tally.max_by { |name, count| [count, name] }&.first || "root"
245
+ end
246
+
247
+ def scope_for(definitions, manifest)
248
+ scopes = definitions.filter_map do |definition|
249
+ relative = manifest.relative_workspace_path(location_path(definition.location))
250
+ next unless relative
251
+
252
+ relative.split(File::SEPARATOR).any? { |segment| TEST_SEGMENTS.include?(segment) } ? 1 : 0
253
+ end.uniq
254
+ scopes.length > 1 ? 2 : scopes.first || 0
255
+ end
256
+
257
+ def site_key(location)
258
+ [location_path(location), location.start_line, location.start_column, location.end_line, location.end_column]
259
+ end
260
+
261
+ def namespace?(declaration)
262
+ declaration.is_a?(Rubydex::Namespace)
263
+ end
264
+
265
+ def canonical_namespace_definition?(declaration, definition)
266
+ (declaration.is_a?(Rubydex::Class) && definition.is_a?(Rubydex::ClassDefinition)) ||
267
+ (declaration.is_a?(Rubydex::Module) && definition.is_a?(Rubydex::ModuleDefinition))
268
+ end
269
+
270
+ def namespace_kind(declaration)
271
+ case declaration
272
+ when Rubydex::Class then 0
273
+ when Rubydex::Module then 1
274
+ else 2
275
+ end
276
+ end
277
+
278
+ def ruby_construct_index(declaration)
279
+ case declaration
280
+ when Rubydex::SingletonClass then nil
281
+ when Rubydex::Class then 0
282
+ when Rubydex::Module then 1
283
+ when Rubydex::Method then 2
284
+ when Rubydex::Constant, Rubydex::ConstantAlias then 3
285
+ end
286
+ end
287
+
288
+ def location_path(location, uri_string = nil)
289
+ uri_string ||= location.uri
290
+ @location_path_cache ||= {}
291
+ return @location_path_cache[uri_string] if @location_path_cache.key?(uri_string)
292
+
293
+ uri = URI.parse(uri_string)
294
+ raise Error, "Rubydex returned a non-file location" unless uri.scheme == "file"
295
+
296
+ @location_path_cache[uri_string] = URI::RFC2396_PARSER.unescape(uri.path)
297
+ end
298
+
299
+ def safe_length(object, method)
300
+ records = object.public_send(method)
301
+ size = records.size
302
+ size.nil? ? safe_count(records) : size
303
+ rescue StandardError
304
+ safe_count(records)
305
+ end
306
+
307
+ def safe_count(records)
308
+ records ? records.count : 0
309
+ rescue StandardError
310
+ 0
311
+ end
312
+
313
+ def project_name(manifest)
314
+ basename = manifest.root.basename.to_s
315
+ return "IRB" if basename.casecmp("irb").zero?
316
+ return "RDoc" if basename.casecmp("rdoc").zero?
317
+
318
+ basename.split(/[-_]+/).map(&:capitalize).join(" ")
319
+ end
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLens
4
+ module Model
5
+ class DependencyAggregation
6
+ DEFAULT_ROW_LIMIT = 18_000
7
+ SIGNAL_COLUMNS = (1..6).freeze
8
+
9
+ def initialize(package_count:, row_limit: DEFAULT_ROW_LIMIT, seed: 0x51A7_E11A)
10
+ raise ArgumentError, "package_count must be nonnegative" if package_count.negative?
11
+ raise ArgumentError, "row_limit must be nonnegative" if row_limit.negative?
12
+
13
+ @row_limit = row_limit
14
+ @random = Random.new(seed)
15
+ @ordinal = 0
16
+ @seen_nonrepresentative = 0
17
+ @counts = Array.new(package_count, 0)
18
+ @ruby_counts = Array.new(package_count) { Array.new(4, 0) }
19
+ @signal_maxima = Array.new(6, 0)
20
+ @representatives = Array.new(package_count)
21
+ @represented_packages = []
22
+ @representative_count = 0
23
+ @nonempty_package_count = 0
24
+ @reservoir = []
25
+ end
26
+
27
+ def add(package_index:, row:, construct_index:)
28
+ @counts[package_index] += 1
29
+ @ruby_counts[package_index][construct_index] += 1 if construct_index
30
+ SIGNAL_COLUMNS.each_with_index do |column, index|
31
+ @signal_maxima[index] = [@signal_maxima[index], row[column]].max
32
+ end
33
+
34
+ entry = [package_index, @ordinal, row.dup.freeze]
35
+ @ordinal += 1
36
+ if @counts[package_index] == 1
37
+ add_first_package_entry(package_index, entry)
38
+ elsif @representatives[package_index] && @random.rand(@counts[package_index]).zero?
39
+ displaced = @representatives[package_index]
40
+ @representatives[package_index] = entry
41
+ add_to_reservoir(displaced)
42
+ else
43
+ add_to_reservoir(entry)
44
+ end
45
+ end
46
+
47
+ def packages
48
+ sampled_rows = Array.new(@counts.length) { [] }
49
+ (@representatives.compact + @reservoir).sort_by { |_package_index, ordinal, _row| ordinal }
50
+ .each { |package_index, _ordinal, row| sampled_rows[package_index] << row }
51
+ @counts.each_index.map do |index|
52
+ {
53
+ declaration_count: @counts[index],
54
+ ruby_counts: @ruby_counts[index].dup.freeze,
55
+ declarations: sampled_rows[index].freeze,
56
+ }.freeze
57
+ end.freeze
58
+ end
59
+
60
+ def signal_maxima
61
+ @signal_maxima.dup.freeze
62
+ end
63
+
64
+ private
65
+
66
+ def add_first_package_entry(package_index, entry)
67
+ @nonempty_package_count += 1
68
+ if @representative_count < @row_limit
69
+ @representatives[package_index] = entry
70
+ @represented_packages << package_index
71
+ @representative_count += 1
72
+ trim_reservoir
73
+ elsif @row_limit.positive? && @random.rand(@nonempty_package_count) < @row_limit
74
+ slot = @random.rand(@represented_packages.length)
75
+ displaced_package = @represented_packages[slot]
76
+ add_to_reservoir(@representatives[displaced_package])
77
+ @representatives[displaced_package] = nil
78
+ @representatives[package_index] = entry
79
+ @represented_packages[slot] = package_index
80
+ else
81
+ add_to_reservoir(entry)
82
+ end
83
+ end
84
+
85
+ def add_to_reservoir(entry)
86
+ capacity = reservoir_capacity
87
+ replacement = @random.rand(@seen_nonrepresentative + 1)
88
+ if @reservoir.length < capacity
89
+ @reservoir << entry
90
+ elsif replacement < capacity
91
+ @reservoir[replacement] = entry
92
+ end
93
+ @seen_nonrepresentative += 1
94
+ end
95
+
96
+ def trim_reservoir
97
+ @reservoir.delete_at(@random.rand(@reservoir.length)) while @reservoir.length > reservoir_capacity
98
+ end
99
+
100
+ def reservoir_capacity
101
+ @row_limit - @representative_count
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLens
4
+ module Paths
5
+ module_function
6
+
7
+ def inside?(path, directory)
8
+ path = File.expand_path(path)
9
+ directory = File.expand_path(directory)
10
+ path == directory || path.start_with?("#{directory}#{File::SEPARATOR}")
11
+ end
12
+ end
13
+ end