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.
- checksums.yaml +4 -4
- data/README.md +67 -39
- data/assets/runtime/report.js +1607 -251
- data/assets/shells/report.html +9 -7
- data/assets/shells/showcase.html +1 -0
- data/assets/styles/report.css +51 -25
- data/assets/styles/showcase.css +3 -0
- data/lib/rubylens/art_model_builder.rb +214 -72
- data/lib/rubylens/artifact_marker.rb +17 -0
- data/lib/rubylens/atomic_output.rb +27 -0
- data/lib/rubylens/cli.rb +53 -36
- data/lib/rubylens/clip/chrome_page.rb +183 -0
- data/lib/rubylens/clip/deadline_io.rb +28 -0
- data/lib/rubylens/clip/renderer.rb +113 -0
- data/lib/rubylens/clip/toolchain.rb +82 -0
- data/lib/rubylens/clip/web_socket_channel.rb +116 -0
- data/lib/rubylens/clip_generator.rb +102 -0
- data/lib/rubylens/default_output.rb +24 -0
- data/lib/rubylens/generator.rb +9 -10
- data/lib/rubylens/git_repository.rb +13 -7
- 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 +62 -157
- data/lib/rubylens/index/rspec_extractor.rb +12 -10
- data/lib/rubylens/index/rubydex_adapter.rb +111 -301
- data/lib/rubylens/index/source_path.rb +12 -11
- data/lib/rubylens/model/dependency_aggregation.rb +11 -7
- data/lib/rubylens/morphology_classifier.rb +52 -12
- data/lib/rubylens/report_writer.rb +5 -20
- data/lib/rubylens/showcase_generator.rb +5 -9
- data/lib/rubylens/showcase_model.rb +12 -14
- data/lib/rubylens/showcase_writer.rb +0 -6
- data/lib/rubylens/version.rb +1 -1
- data/lib/rubylens.rb +7 -0
- metadata +16 -3
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "artifact_marker"
|
|
4
|
+
require_relative "atomic_output"
|
|
5
|
+
require_relative "default_output"
|
|
6
|
+
require_relative "showcase_generator"
|
|
7
|
+
require_relative "clip/renderer"
|
|
8
|
+
require_relative "clip/toolchain"
|
|
9
|
+
|
|
10
|
+
module RubyLens
|
|
11
|
+
ClipResult = Data.define(:output_path, :showcase_path, :counts, :warnings) do
|
|
12
|
+
def to_payload = { output: output_path, showcase: showcase_path, counts: counts, warnings: warnings }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Generates a shareable MP4 of a project's showcase. The showcase HTML is
|
|
16
|
+
# written first (it is the render input and stays useful on its own), then
|
|
17
|
+
# headless Chrome and ffmpeg turn it into one seamless 1080p camera loop.
|
|
18
|
+
# The toolchain is checked before any indexing so a missing browser or
|
|
19
|
+
# ffmpeg fails in under a second with install guidance.
|
|
20
|
+
class ClipGenerator
|
|
21
|
+
DEFAULT_CLIP_NAME = "rubylens-clip.mp4"
|
|
22
|
+
# faststart parks the metadata (and so the marker) right behind ftyp, so
|
|
23
|
+
# every clip RubyLens writes matches within this head window.
|
|
24
|
+
MARKER_SCAN_HEAD_BYTES = 512 * 1024
|
|
25
|
+
|
|
26
|
+
def initialize(path: Dir.pwd, output: nil, lockfile: nil, details: false, progress: nil, renderer: nil)
|
|
27
|
+
@path = path
|
|
28
|
+
@output = output
|
|
29
|
+
@lockfile = lockfile
|
|
30
|
+
@details = details
|
|
31
|
+
@progress = progress
|
|
32
|
+
@renderer = renderer
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call
|
|
36
|
+
renderer = @renderer || build_renderer
|
|
37
|
+
root = File.realpath(@path)
|
|
38
|
+
output, showcase_output = resolve_outputs(root)
|
|
39
|
+
showcase_result = ShowcaseGenerator.new(
|
|
40
|
+
path: @path, output: showcase_output, lockfile: @lockfile, details: @details,
|
|
41
|
+
).call
|
|
42
|
+
render_atomically(renderer, showcase_result.output_path, output)
|
|
43
|
+
|
|
44
|
+
ClipResult.new(
|
|
45
|
+
output_path: output,
|
|
46
|
+
showcase_path: showcase_result.output_path,
|
|
47
|
+
counts: showcase_result.counts,
|
|
48
|
+
warnings: showcase_result.warnings,
|
|
49
|
+
)
|
|
50
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP => error
|
|
51
|
+
raise Error, error.message
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def build_renderer
|
|
57
|
+
Clip::Renderer.new(
|
|
58
|
+
chrome: Clip::Toolchain.chrome_path,
|
|
59
|
+
ffmpeg: Clip::Toolchain.ffmpeg_path,
|
|
60
|
+
progress: @progress,
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# With no --output, both artifacts use their default names, Git-excluded
|
|
65
|
+
# locally, and an existing default clip is only replaced if it is ours.
|
|
66
|
+
# With --output FILE.mp4, the MP4 is written exactly where requested, like
|
|
67
|
+
# other custom output paths; the derived FILE.html companion was never
|
|
68
|
+
# named by the user, so an unrelated existing file there is refused.
|
|
69
|
+
def resolve_outputs(root)
|
|
70
|
+
if @output.nil?
|
|
71
|
+
default = DefaultOutput.resolve(root: root, name: DEFAULT_CLIP_NAME, description: "clip") do |existing|
|
|
72
|
+
ArtifactMarker.present?(existing, Clip::Renderer::MARKER_COMMENT, head_bytes: MARKER_SCAN_HEAD_BYTES)
|
|
73
|
+
end
|
|
74
|
+
return [default, nil]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Expanded once here so the guard, the writer, and the reported result
|
|
78
|
+
# all refer to the same real paths.
|
|
79
|
+
output = File.expand_path(@output)
|
|
80
|
+
companion = showcase_companion_path(output)
|
|
81
|
+
if File.exist?(companion) && !ArtifactMarker.present?(companion, ShowcaseWriter::MARKER)
|
|
82
|
+
raise Error, "clip companion path #{companion} already exists and is not a RubyLens showcase"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
[output, companion]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def showcase_companion_path(output)
|
|
89
|
+
base = output.sub(/\.mp4\z/i, "")
|
|
90
|
+
base = output if base.empty?
|
|
91
|
+
"#{base}.html"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def render_atomically(renderer, showcase_html, output)
|
|
95
|
+
AtomicOutput.replace(output) do |temporary|
|
|
96
|
+
renderer.render(showcase_html: showcase_html, output: temporary)
|
|
97
|
+
rescue Error => error
|
|
98
|
+
raise Error, "#{error.message} (the showcase HTML was still written to #{showcase_html})"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "git_repository"
|
|
5
|
+
|
|
6
|
+
module RubyLens
|
|
7
|
+
# Safety policy shared by every generator's default output path: an existing
|
|
8
|
+
# file is only replaced when the given check recognizes it as our own kind of
|
|
9
|
+
# artifact, and the path plus its temporary-file pattern are added to the
|
|
10
|
+
# repository's local Git excludes.
|
|
11
|
+
module DefaultOutput
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def resolve(root:, name:, description:)
|
|
15
|
+
output = File.join(root, name)
|
|
16
|
+
if File.exist?(output) && !yield(output)
|
|
17
|
+
raise Error, "default #{description} path already exists and is not a RubyLens #{description}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
GitRepository.new(root).exclude_local(output, description: description)
|
|
21
|
+
output
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/rubylens/generator.rb
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "artifact_marker"
|
|
4
|
+
require_relative "default_output"
|
|
5
|
+
|
|
3
6
|
module RubyLens
|
|
4
|
-
Result = Data.define(:output_path, :counts, :warnings)
|
|
7
|
+
Result = Data.define(:output_path, :counts, :warnings) do
|
|
8
|
+
def to_payload = { output: output_path, counts: counts, warnings: warnings }
|
|
9
|
+
end
|
|
5
10
|
|
|
6
11
|
class GenerationPipeline
|
|
7
12
|
def initialize(root:, lockfile: nil)
|
|
@@ -32,17 +37,11 @@ module RubyLens
|
|
|
32
37
|
|
|
33
38
|
def call
|
|
34
39
|
root = File.realpath(@path)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if output.nil?
|
|
38
|
-
output = File.join(root, DEFAULT_REPORT_NAME)
|
|
39
|
-
if File.exist?(output) && !report_writer.rubylens_report?(output)
|
|
40
|
-
raise Error, "default report path already exists and is not a RubyLens report"
|
|
41
|
-
end
|
|
42
|
-
GitRepository.new(root).exclude_local(output)
|
|
40
|
+
output = @output || DefaultOutput.resolve(root: root, name: DEFAULT_REPORT_NAME, description: "report") do |existing|
|
|
41
|
+
ArtifactMarker.present?(existing, ReportWriter::MARKER)
|
|
43
42
|
end
|
|
44
43
|
model, warnings = GenerationPipeline.new(root:, lockfile: @lockfile).call
|
|
45
|
-
output_path =
|
|
44
|
+
output_path = ReportWriter.new.write(model, output: output)
|
|
46
45
|
|
|
47
46
|
RubyLens::Result.new(output_path: output_path, counts: model.fetch("totals").freeze, warnings:)
|
|
48
47
|
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP => error
|
|
@@ -22,19 +22,25 @@ module RubyLens
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def selected_files
|
|
25
|
-
|
|
25
|
+
arguments = ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]
|
|
26
|
+
unless @target_root == @git_root
|
|
27
|
+
relative_target = @target_root.relative_path_from(@git_root)
|
|
28
|
+
arguments.concat(["--", ":(literal)#{relative_target}"])
|
|
29
|
+
end
|
|
30
|
+
output, status = capture(*arguments)
|
|
26
31
|
raise GitError, "failed to enumerate tracked and unignored files" unless status.success?
|
|
27
32
|
|
|
33
|
+
git_root = @git_root.to_s
|
|
34
|
+
target_root = @target_root.to_s
|
|
28
35
|
output.split("\0").filter_map do |relative_to_git|
|
|
29
36
|
next unless INDEXABLE_EXTENSIONS.include?(File.extname(relative_to_git))
|
|
30
37
|
|
|
31
|
-
absolute =
|
|
32
|
-
next unless
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
next unless Paths.inside?(resolved, @target_root.realpath)
|
|
38
|
+
absolute = File.join(git_root, relative_to_git)
|
|
39
|
+
next unless File.file?(absolute)
|
|
40
|
+
resolved = File.realpath(absolute)
|
|
41
|
+
next unless Paths.inside?(resolved, target_root)
|
|
36
42
|
|
|
37
|
-
resolved
|
|
43
|
+
resolved
|
|
38
44
|
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
39
45
|
nil
|
|
40
46
|
end.sort
|
|
@@ -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
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubydex"
|
|
4
|
+
require_relative "../model/dependency_aggregation"
|
|
5
|
+
|
|
6
|
+
module RubyLens
|
|
7
|
+
module Index
|
|
8
|
+
# Streams Rubydex's declarations once and splits them into the three
|
|
9
|
+
# populations the snapshot needs: workspace namespaces that become scene
|
|
10
|
+
# points, Ruby construct tallies per category, and dependency declaration
|
|
11
|
+
# rows aggregated per package.
|
|
12
|
+
#
|
|
13
|
+
# It is a single pass because Rubydex materializes a new wrapper for every
|
|
14
|
+
# accessor call, so revisiting the stream costs as much as producing it.
|
|
15
|
+
class DeclarationCollector
|
|
16
|
+
# Shapes shared with the other collectors. A site key is a definition's
|
|
17
|
+
# location flattened for comparison and grouping; a definition range is a
|
|
18
|
+
# namespace's span within one file, tagged with the namespace's ordinal.
|
|
19
|
+
#
|
|
20
|
+
# @rbs!
|
|
21
|
+
# type site_key = [String, Integer, Integer, Integer, Integer]
|
|
22
|
+
# type definition_range = [Integer, Integer, Integer, Integer, Integer]
|
|
23
|
+
# type dependency_package = { ruby_counts: Array[Integer], declarations: Array[Array[Integer]] }
|
|
24
|
+
|
|
25
|
+
# A workspace namespace that earned a scene point, with the canonical
|
|
26
|
+
# definition sites that decide where its constant references land.
|
|
27
|
+
Namespace = Data.define(
|
|
28
|
+
:declaration, #: Rubydex::Namespace
|
|
29
|
+
:name, #: String
|
|
30
|
+
:definition_sites, #: Integer
|
|
31
|
+
:scope, #: Integer
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
Result = Data.define(
|
|
35
|
+
:namespaces, #: Array[Namespace]
|
|
36
|
+
:definition_ranges, #: Hash[String, Array[definition_range]]
|
|
37
|
+
:category_stats, #: Hash[String, Array[Integer]]
|
|
38
|
+
:dependency_packages, #: Array[dependency_package]
|
|
39
|
+
:dependency_signal_maxima, #: Array[Integer]
|
|
40
|
+
:dependency_ordinal_by_name, #: Hash[String, Integer]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
CLASS = 0
|
|
44
|
+
MODULE = 1
|
|
45
|
+
METHOD = 2
|
|
46
|
+
CONSTANT = 3
|
|
47
|
+
OTHER_KIND = 2
|
|
48
|
+
MIXED_SCOPE = 2
|
|
49
|
+
|
|
50
|
+
#: (manifest: untyped, locations: LocationIndex) -> void
|
|
51
|
+
def initialize(manifest:, locations:)
|
|
52
|
+
@manifest = manifest
|
|
53
|
+
@locations = locations
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#: (Enumerable[Rubydex::Declaration]) -> Result
|
|
57
|
+
def call(declarations)
|
|
58
|
+
namespaces = []
|
|
59
|
+
category_stats = { "core" => Array.new(4, 0), "tests" => Array.new(4, 0) }
|
|
60
|
+
aggregation = Model::DependencyAggregation.new(package_count: @manifest.packages.length)
|
|
61
|
+
definition_ranges = Hash.new { |ranges, uri| ranges[uri] = [] }
|
|
62
|
+
dependency_positions = {}
|
|
63
|
+
|
|
64
|
+
declarations.each do |declaration|
|
|
65
|
+
name = declaration.name
|
|
66
|
+
next unless eligible?(declaration, name)
|
|
67
|
+
|
|
68
|
+
namespace = declaration.is_a?(Rubydex::Namespace)
|
|
69
|
+
construct = construct_index(declaration)
|
|
70
|
+
# Destructured immediately rather than wrapped: this runs once per
|
|
71
|
+
# declaration in the largest loop RubyLens has, and the names below
|
|
72
|
+
# are the only place these five values are ever read.
|
|
73
|
+
workspace_count, tests_only, site_keys, canonical_scope, package_site_keys =
|
|
74
|
+
summarize(declaration, namespace)
|
|
75
|
+
|
|
76
|
+
if site_keys
|
|
77
|
+
site_keys = site_keys.uniq if site_keys.length > 1
|
|
78
|
+
ordinal = namespaces.length
|
|
79
|
+
site_keys.each do |uri, start_line, start_column, end_line, end_column|
|
|
80
|
+
definition_ranges[uri] << [start_line, start_column, end_line, end_column, ordinal]
|
|
81
|
+
end
|
|
82
|
+
namespaces << Namespace.new(
|
|
83
|
+
declaration: declaration,
|
|
84
|
+
name: name,
|
|
85
|
+
definition_sites: site_keys.length,
|
|
86
|
+
scope: canonical_scope,
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
if construct && workspace_count.positive?
|
|
90
|
+
category_stats.fetch(tests_only ? "tests" : "core")[construct] += 1
|
|
91
|
+
end
|
|
92
|
+
if package_site_keys
|
|
93
|
+
collect_dependency(
|
|
94
|
+
aggregation, dependency_positions, declaration, name, namespace, construct, package_site_keys
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Result.new(
|
|
100
|
+
namespaces: namespaces,
|
|
101
|
+
definition_ranges: definition_ranges,
|
|
102
|
+
category_stats: category_stats,
|
|
103
|
+
dependency_packages: aggregation.packages,
|
|
104
|
+
dependency_signal_maxima: aggregation.signal_maxima,
|
|
105
|
+
dependency_ordinal_by_name: global_dependency_ordinals(aggregation.packages, dependency_positions),
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Rubydex reports synthetic declarations RubyLens never draws: anonymous
|
|
110
|
+
# namespaces, Todo placeholders standing in for unresolved constants, and
|
|
111
|
+
# singleton classes attached to either.
|
|
112
|
+
#
|
|
113
|
+
#: (Rubydex::Declaration, ?String?) -> bool
|
|
114
|
+
def eligible?(declaration, name = declaration.name)
|
|
115
|
+
return false if name.nil? || name.empty? || name.include?("<anonymous>")
|
|
116
|
+
return false if declaration.is_a?(Rubydex::Todo)
|
|
117
|
+
return true unless declaration.is_a?(Rubydex::SingletonClass)
|
|
118
|
+
|
|
119
|
+
attached = declaration.attached_class
|
|
120
|
+
!attached.is_a?(Rubydex::SingletonClass) && !attached.is_a?(Rubydex::Todo)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
#: (Rubydex::Declaration) -> Integer?
|
|
124
|
+
def construct_index(declaration)
|
|
125
|
+
case declaration
|
|
126
|
+
when Rubydex::SingletonClass then nil
|
|
127
|
+
when Rubydex::Class then CLASS
|
|
128
|
+
when Rubydex::Module then MODULE
|
|
129
|
+
when Rubydex::Method then METHOD
|
|
130
|
+
when Rubydex::Constant, Rubydex::ConstantAlias then CONSTANT
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
# One pass over a declaration's definitions, so each definition's location
|
|
137
|
+
# and its per-URI workspace, scope, and package answers are read once.
|
|
138
|
+
# Returns [workspace_count, tests_only, canonical_site_keys,
|
|
139
|
+
# canonical_scope, package_site_keys]; the site-key collections stay nil
|
|
140
|
+
# until a definition contributes one, since most declarations contribute
|
|
141
|
+
# neither a canonical namespace site nor a package site.
|
|
142
|
+
#
|
|
143
|
+
#: (Rubydex::Declaration, bool) -> [Integer, bool, Array[site_key]?, Integer, Hash[Integer, Array[site_key]]?]
|
|
144
|
+
def summarize(declaration, namespace)
|
|
145
|
+
workspace_count = 0
|
|
146
|
+
tests_seen = false
|
|
147
|
+
core_seen = false
|
|
148
|
+
canonical_tests_seen = false
|
|
149
|
+
canonical_core_seen = false
|
|
150
|
+
canonical_site_keys = nil
|
|
151
|
+
package_site_keys = nil
|
|
152
|
+
|
|
153
|
+
declaration.definitions.each do |definition|
|
|
154
|
+
location = definition.location
|
|
155
|
+
site_key = nil
|
|
156
|
+
if @locations.workspace?(location)
|
|
157
|
+
workspace_count += 1
|
|
158
|
+
scope = @locations.scope_for(location.uri)
|
|
159
|
+
if scope == LocationIndex::TEST
|
|
160
|
+
tests_seen = true
|
|
161
|
+
elsif scope
|
|
162
|
+
core_seen = true
|
|
163
|
+
end
|
|
164
|
+
if namespace && canonical_definition?(declaration, definition)
|
|
165
|
+
(canonical_site_keys ||= []) << (site_key = location.comparable_values)
|
|
166
|
+
if scope == LocationIndex::TEST
|
|
167
|
+
canonical_tests_seen = true
|
|
168
|
+
elsif scope
|
|
169
|
+
canonical_core_seen = true
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
package_index = @locations.package_index_for(location)
|
|
174
|
+
if package_index
|
|
175
|
+
((package_site_keys ||= {})[package_index] ||= []) << (site_key || location.comparable_values)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
[
|
|
180
|
+
workspace_count,
|
|
181
|
+
tests_seen && !core_seen,
|
|
182
|
+
canonical_site_keys,
|
|
183
|
+
canonical_scope(canonical_tests_seen, canonical_core_seen),
|
|
184
|
+
package_site_keys,
|
|
185
|
+
]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
#: (bool, bool) -> Integer
|
|
189
|
+
def canonical_scope(tests_seen, core_seen)
|
|
190
|
+
return LocationIndex::CORE unless tests_seen
|
|
191
|
+
|
|
192
|
+
core_seen ? MIXED_SCOPE : LocationIndex::TEST
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# A declaration reopened across packages is attributed to the one holding
|
|
196
|
+
# the most of its definitions, ties going to the earliest package.
|
|
197
|
+
#
|
|
198
|
+
#: (Model::DependencyAggregation, Hash[String, [Integer, Integer]], Rubydex::Declaration, String, bool, Integer?, Hash[Integer, Array[site_key]]) -> void
|
|
199
|
+
def collect_dependency(aggregation, positions, declaration, name, namespace, construct, package_site_keys)
|
|
200
|
+
package_index = nil
|
|
201
|
+
site_keys = nil
|
|
202
|
+
package_site_keys.each do |index, keys|
|
|
203
|
+
if site_keys.nil? || keys.length > site_keys.length ||
|
|
204
|
+
(keys.length == site_keys.length && index < package_index)
|
|
205
|
+
package_index = index
|
|
206
|
+
site_keys = keys
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
sites = site_keys.length > 1 ? site_keys.uniq.length : site_keys.length
|
|
211
|
+
references = length_of(declaration, :references)
|
|
212
|
+
row = [
|
|
213
|
+
namespace_kind(declaration),
|
|
214
|
+
namespace ? [declaration.ancestors.count - 1, 0].max : 0,
|
|
215
|
+
sites,
|
|
216
|
+
[sites - 1, 0].max,
|
|
217
|
+
namespace ? [declaration.descendants.count - 1, 0].max : 0,
|
|
218
|
+
references,
|
|
219
|
+
namespace ? length_of(declaration, :members) : 0,
|
|
220
|
+
].freeze
|
|
221
|
+
local_index = aggregation.add(package_index: package_index, row: row, construct_index: construct)
|
|
222
|
+
if references.positive? && constant_reference_target?(declaration, namespace)
|
|
223
|
+
positions[name] = [package_index, local_index]
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Dependency stars are addressed by a single ordinal spanning every
|
|
228
|
+
# package, so per-package positions are rebased onto that flat sequence.
|
|
229
|
+
#
|
|
230
|
+
#: (Array[dependency_package], Hash[String, [Integer, Integer]]) -> Hash[String, Integer]
|
|
231
|
+
def global_dependency_ordinals(packages, positions)
|
|
232
|
+
offsets = []
|
|
233
|
+
offset = 0
|
|
234
|
+
packages.each do |package|
|
|
235
|
+
offsets << offset
|
|
236
|
+
offset += package.fetch(:declarations).length
|
|
237
|
+
end
|
|
238
|
+
positions.transform_values! do |package_index, local_index|
|
|
239
|
+
offsets.fetch(package_index) + local_index
|
|
240
|
+
end
|
|
241
|
+
positions.freeze
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
#: (Rubydex::Declaration) -> Integer
|
|
245
|
+
def namespace_kind(declaration)
|
|
246
|
+
case declaration
|
|
247
|
+
when Rubydex::Class then CLASS
|
|
248
|
+
when Rubydex::Module then MODULE
|
|
249
|
+
else OTHER_KIND
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
#: (Rubydex::Declaration, bool) -> bool
|
|
254
|
+
def constant_reference_target?(declaration, namespace)
|
|
255
|
+
namespace ||
|
|
256
|
+
declaration.is_a?(Rubydex::Constant) ||
|
|
257
|
+
declaration.is_a?(Rubydex::ConstantAlias)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
#: (Rubydex::Declaration, Rubydex::Definition) -> bool
|
|
261
|
+
def canonical_definition?(declaration, definition)
|
|
262
|
+
(declaration.is_a?(Rubydex::Class) && definition.is_a?(Rubydex::ClassDefinition)) ||
|
|
263
|
+
(declaration.is_a?(Rubydex::Module) && definition.is_a?(Rubydex::ModuleDefinition))
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Rubydex collections expose size on some shapes and only count on others,
|
|
267
|
+
# and this pre-1.0 interface can raise from either. A dependency star that
|
|
268
|
+
# cannot report a signal is still a real declaration, so an unreadable
|
|
269
|
+
# count degrades to zero rather than losing the row.
|
|
270
|
+
#
|
|
271
|
+
#: (untyped, Symbol) -> Integer
|
|
272
|
+
def length_of(object, name)
|
|
273
|
+
records = object.public_send(name)
|
|
274
|
+
size = records.size
|
|
275
|
+
size.nil? ? count_of(records) : size
|
|
276
|
+
rescue StandardError
|
|
277
|
+
count_of(records)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
#: (untyped) -> Integer
|
|
281
|
+
def count_of(records)
|
|
282
|
+
records ? records.count : 0
|
|
283
|
+
rescue StandardError
|
|
284
|
+
0
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|