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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +67 -0
- data/assets/runtime/report.js +1409 -0
- data/assets/shells/report.html +34 -0
- data/assets/shells/showcase.html +26 -0
- data/assets/styles/report.css +85 -0
- data/assets/styles/showcase.css +15 -0
- data/exe/rubylens +6 -0
- data/lib/rubylens/art_model_builder.rb +94 -0
- data/lib/rubylens/cli.rb +104 -0
- data/lib/rubylens/errors.rb +6 -0
- data/lib/rubylens/generator.rb +56 -0
- data/lib/rubylens/git_repository.rb +106 -0
- data/lib/rubylens/index/manifest.rb +231 -0
- data/lib/rubylens/index/rubydex_adapter.rb +322 -0
- data/lib/rubylens/model/dependency_aggregation.rb +105 -0
- data/lib/rubylens/paths.rb +13 -0
- data/lib/rubylens/report_asset_assembler.rb +38 -0
- data/lib/rubylens/report_writer.rb +61 -0
- data/lib/rubylens/showcase_generator.rb +41 -0
- data/lib/rubylens/showcase_model.rb +41 -0
- data/lib/rubylens/showcase_writer.rb +27 -0
- data/lib/rubylens/version.rb +5 -0
- data/lib/rubylens.rb +33 -0
- metadata +99 -0
|
@@ -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
|