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,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "set"
|
|
6
|
+
|
|
7
|
+
module RubyLens
|
|
8
|
+
module Index
|
|
9
|
+
# Resolves a git-sourced lockfile entry into the exact files RubyLens may index.
|
|
10
|
+
#
|
|
11
|
+
# Git checkouts carry more authority than RubyGems packages: the gemspec is
|
|
12
|
+
# code from the checkout that names its own require paths, and the tree may
|
|
13
|
+
# hold symlinks pointing anywhere on disk. Every path here is therefore
|
|
14
|
+
# resolved and re-checked against the canonical package root before it
|
|
15
|
+
# becomes indexable, and any breach ends the package rather than narrowing
|
|
16
|
+
# it, so a partially trusted tree can never contribute files.
|
|
17
|
+
class GitPackageSource
|
|
18
|
+
UnsafeRequirePath = Class.new(StandardError)
|
|
19
|
+
UnsafePackageFile = Class.new(StandardError)
|
|
20
|
+
|
|
21
|
+
# Logical roots come from the gemspec and may be symlinked; the canonical
|
|
22
|
+
# root is the resolved directory every indexable file must sit inside.
|
|
23
|
+
PackagePaths = Data.define(
|
|
24
|
+
:logical_root, #: Pathname
|
|
25
|
+
:canonical_root, #: Pathname
|
|
26
|
+
:logical_canonical_root, #: Pathname
|
|
27
|
+
)
|
|
28
|
+
Resolution = Data.define(
|
|
29
|
+
:root, #: Pathname
|
|
30
|
+
:files, #: Array[String]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Nix stores gems in immutable, content-addressed store objects, so a
|
|
34
|
+
# checkout symlinked into one is trustworthy even though it resolves
|
|
35
|
+
# outside the bundle directory.
|
|
36
|
+
class NixStoreProvider
|
|
37
|
+
STORE_ROOT = Pathname("/nix/store").freeze
|
|
38
|
+
STORE_OBJECT_PATTERN = /\A[0123456789abcdfghijklmnpqrsvwxyz]{32}-.+\z/
|
|
39
|
+
|
|
40
|
+
#: (Pathname | String) -> bool
|
|
41
|
+
def trusted?(path)
|
|
42
|
+
path = Pathname(path).expand_path
|
|
43
|
+
return false unless Paths.inside?(path, STORE_ROOT)
|
|
44
|
+
|
|
45
|
+
first_component = path.relative_path_from(STORE_ROOT).each_filename.first
|
|
46
|
+
return false unless first_component
|
|
47
|
+
|
|
48
|
+
STORE_OBJECT_PATTERN.match?(first_component)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
#: (lockfile: Pathname) -> void
|
|
53
|
+
def initialize(lockfile:)
|
|
54
|
+
@lockfile = lockfile
|
|
55
|
+
@nix_store_provider = NixStoreProvider.new
|
|
56
|
+
@specification_indexes = {}.compare_by_identity
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns a Resolution, or a Symbol naming why the package was skipped.
|
|
60
|
+
# Callers own how a skip is reported, so every reason surfaces through the
|
|
61
|
+
# same warning vocabulary as the other package sources.
|
|
62
|
+
#
|
|
63
|
+
#: (Bundler::LazySpecification) -> (Resolution | Symbol)
|
|
64
|
+
def resolve(locked)
|
|
65
|
+
source = locked.source
|
|
66
|
+
return :local_only_required if source.allow_git_ops?
|
|
67
|
+
|
|
68
|
+
checkout = checkout_path(source)
|
|
69
|
+
return :checkout_unavailable unless checkout.directory?
|
|
70
|
+
|
|
71
|
+
index = (@specification_indexes[source] ||= specification_index(source, checkout))
|
|
72
|
+
specification = index.search(locked).first
|
|
73
|
+
return :specification_unavailable unless specification
|
|
74
|
+
|
|
75
|
+
paths = package_paths(specification, checkout)
|
|
76
|
+
return :unsafe_specification_root unless paths
|
|
77
|
+
|
|
78
|
+
Resolution.new(
|
|
79
|
+
root: paths.canonical_root,
|
|
80
|
+
files: indexable_files(paths, specification.require_paths).freeze,
|
|
81
|
+
)
|
|
82
|
+
rescue UnsafeRequirePath
|
|
83
|
+
:unsafe_require_paths
|
|
84
|
+
rescue UnsafePackageFile
|
|
85
|
+
:unsafe_package_files
|
|
86
|
+
rescue Bundler::BundlerError, Gem::Exception
|
|
87
|
+
:specification_unreadable
|
|
88
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
89
|
+
:checkout_unavailable
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
#: (Bundler::Source::Git) -> Pathname
|
|
95
|
+
def checkout_path(source)
|
|
96
|
+
bundle_root = @lockfile.dirname
|
|
97
|
+
app_config = ENV["BUNDLE_APP_CONFIG"]
|
|
98
|
+
app_config_path = if app_config
|
|
99
|
+
Pathname(app_config).expand_path(bundle_root)
|
|
100
|
+
else
|
|
101
|
+
bundle_root.join(".bundle")
|
|
102
|
+
end
|
|
103
|
+
bundle_path = Pathname(Bundler::Settings.new(app_config_path).path.path).expand_path(bundle_root)
|
|
104
|
+
bundle_path.join("bundler/gems", source.extension_dir_name)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
#: (Bundler::Source::Git, Pathname) -> Bundler::Index
|
|
108
|
+
def specification_index(source, checkout)
|
|
109
|
+
source.__send__(:set_install_path!, checkout)
|
|
110
|
+
source.specs
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
#: (Gem::Specification, Pathname) -> PackagePaths?
|
|
114
|
+
def package_paths(specification, checkout)
|
|
115
|
+
logical_root = Pathname(specification.full_gem_path).expand_path
|
|
116
|
+
logical_gemspec = Pathname(specification.loaded_from).expand_path
|
|
117
|
+
return unless Paths.inside?(logical_root, checkout)
|
|
118
|
+
return unless logical_gemspec.dirname == logical_root
|
|
119
|
+
return unless logical_gemspec.file? && logical_root.directory?
|
|
120
|
+
|
|
121
|
+
canonical_checkout = checkout.realpath
|
|
122
|
+
resolved_gemspec_root = logical_gemspec.realpath.dirname
|
|
123
|
+
logical_canonical_root = logical_root.realpath
|
|
124
|
+
return unless resolved_gemspec_root.directory?
|
|
125
|
+
|
|
126
|
+
normal_checkout = !checkout.symlink? &&
|
|
127
|
+
Paths.inside?(resolved_gemspec_root, canonical_checkout) &&
|
|
128
|
+
Paths.inside?(logical_canonical_root, canonical_checkout)
|
|
129
|
+
return unless normal_checkout || @nix_store_provider.trusted?(resolved_gemspec_root)
|
|
130
|
+
|
|
131
|
+
PackagePaths.new(
|
|
132
|
+
logical_root: logical_root,
|
|
133
|
+
canonical_root: normal_checkout ? logical_canonical_root : resolved_gemspec_root,
|
|
134
|
+
logical_canonical_root: logical_canonical_root,
|
|
135
|
+
)
|
|
136
|
+
rescue TypeError, ArgumentError
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
#: (PackagePaths, Array[String]) -> Array[String]
|
|
141
|
+
def indexable_files(paths, require_paths)
|
|
142
|
+
logical_require_paths = require_paths.map do |relative_path|
|
|
143
|
+
raise UnsafeRequirePath unless relative_path.is_a?(String)
|
|
144
|
+
|
|
145
|
+
relative = Pathname(relative_path)
|
|
146
|
+
raise UnsafeRequirePath if relative.absolute? || relative.each_filename.include?("..")
|
|
147
|
+
|
|
148
|
+
candidate = paths.logical_root.join(relative).cleanpath
|
|
149
|
+
raise UnsafeRequirePath unless Paths.inside?(candidate, paths.logical_root)
|
|
150
|
+
|
|
151
|
+
candidate
|
|
152
|
+
rescue ArgumentError, EncodingError
|
|
153
|
+
raise UnsafeRequirePath
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
files = []
|
|
157
|
+
visited_directories = Set.new
|
|
158
|
+
logical_require_paths.each do |path|
|
|
159
|
+
next unless path.exist? || path.symlink?
|
|
160
|
+
|
|
161
|
+
traverse(path, paths, files, visited_directories, Set.new)
|
|
162
|
+
end
|
|
163
|
+
files.uniq.sort
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Descends one require path. `active_directories` carries the current
|
|
167
|
+
# branch so a symlink cycle raises instead of recursing forever, while
|
|
168
|
+
# `visited_directories` spans the whole walk so a diamond of symlinks is
|
|
169
|
+
# merely skipped.
|
|
170
|
+
#
|
|
171
|
+
#: (Pathname, PackagePaths, Array[String], Set[Pathname], Set[Pathname]) -> void
|
|
172
|
+
def traverse(path, paths, files, visited_directories, active_directories)
|
|
173
|
+
resolved = path.realpath
|
|
174
|
+
if resolved.file?
|
|
175
|
+
raise UnsafePackageFile unless Paths.inside?(resolved, paths.canonical_root)
|
|
176
|
+
|
|
177
|
+
files << resolved.to_s if INDEXABLE_EXTENSIONS.include?(path.extname)
|
|
178
|
+
return
|
|
179
|
+
end
|
|
180
|
+
return unless resolved.directory?
|
|
181
|
+
|
|
182
|
+
unless Paths.inside?(resolved, paths.logical_canonical_root) || Paths.inside?(resolved, paths.canonical_root)
|
|
183
|
+
raise UnsafePackageFile
|
|
184
|
+
end
|
|
185
|
+
raise UnsafePackageFile if active_directories.include?(resolved)
|
|
186
|
+
return if visited_directories.include?(resolved)
|
|
187
|
+
|
|
188
|
+
active_directories.add(resolved)
|
|
189
|
+
begin
|
|
190
|
+
path.children.sort_by(&:to_s).each do |child|
|
|
191
|
+
traverse(child, paths, files, visited_directories, active_directories)
|
|
192
|
+
end
|
|
193
|
+
visited_directories.add(resolved)
|
|
194
|
+
ensure
|
|
195
|
+
active_directories.delete(resolved)
|
|
196
|
+
end
|
|
197
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
198
|
+
raise UnsafePackageFile
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require_relative "source_path"
|
|
5
|
+
|
|
6
|
+
module RubyLens
|
|
7
|
+
module Index
|
|
8
|
+
# Answers the questions the collectors ask about a Rubydex location: which
|
|
9
|
+
# file it names, whether that file is in the workspace, whether it is test
|
|
10
|
+
# or core code, and which package owns it.
|
|
11
|
+
#
|
|
12
|
+
# Rubydex hands out a fresh wrapper and a fresh URI string on every call and
|
|
13
|
+
# a codebase has far more definitions than files, so each answer is memoized
|
|
14
|
+
# by URI. That memoization is why the collectors can afford to ask per
|
|
15
|
+
# definition instead of hoisting the questions themselves.
|
|
16
|
+
class LocationIndex
|
|
17
|
+
# 1 for a file under a test directory, 0 for any other workspace file.
|
|
18
|
+
TEST = 1
|
|
19
|
+
CORE = 0
|
|
20
|
+
TEST_SEGMENTS = %w[test tests spec specs feature features].freeze
|
|
21
|
+
|
|
22
|
+
#: Set[String]
|
|
23
|
+
attr_reader :package_document_paths
|
|
24
|
+
|
|
25
|
+
# The manifest is duck-typed: production passes a Manifest, tests pass
|
|
26
|
+
# stubs answering the same four questions.
|
|
27
|
+
#
|
|
28
|
+
#: (untyped manifest) -> void
|
|
29
|
+
def initialize(manifest)
|
|
30
|
+
@manifest = manifest
|
|
31
|
+
@path_by_uri = {}
|
|
32
|
+
@workspace_by_uri = {}
|
|
33
|
+
@scope_by_uri = {}
|
|
34
|
+
@package_index_by_uri = {}
|
|
35
|
+
@package_document_paths = Set.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# One sweep over the indexed documents resolves each URI once, recording
|
|
39
|
+
# which of them belong to audited packages and returning the
|
|
40
|
+
# [document, path] pairs so callers need not re-enumerate or re-parse.
|
|
41
|
+
#
|
|
42
|
+
#: (Rubydex::Graph) -> Array[[Rubydex::Document, String]]
|
|
43
|
+
def resolve_documents(graph)
|
|
44
|
+
audited = @manifest.packages.flat_map(&:files).to_set
|
|
45
|
+
documents_with_paths = []
|
|
46
|
+
graph.documents.each do |document|
|
|
47
|
+
path = path_for(document.uri)
|
|
48
|
+
next unless path
|
|
49
|
+
|
|
50
|
+
documents_with_paths << [document, path]
|
|
51
|
+
@package_document_paths << path if audited.include?(path)
|
|
52
|
+
end
|
|
53
|
+
documents_with_paths
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#: (String) -> String?
|
|
57
|
+
def path_for(uri)
|
|
58
|
+
@path_by_uri.fetch(uri) do
|
|
59
|
+
@path_by_uri[uri] = SourcePath.from_file_uri(uri)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
#: (Rubydex::Location) -> bool
|
|
64
|
+
def workspace?(location)
|
|
65
|
+
uri = location.uri
|
|
66
|
+
@workspace_by_uri.fetch(uri) do
|
|
67
|
+
path = path_for(uri)
|
|
68
|
+
@workspace_by_uri[uri] = path ? @manifest.workspace_path?(path) : false
|
|
69
|
+
end
|
|
70
|
+
rescue StandardError
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# TEST, CORE, or nil when the URI has no workspace-relative path.
|
|
75
|
+
#
|
|
76
|
+
#: (String) -> Integer?
|
|
77
|
+
def scope_for(uri)
|
|
78
|
+
@scope_by_uri.fetch(uri) do
|
|
79
|
+
relative = @manifest.relative_workspace_path(path_for(uri))
|
|
80
|
+
@scope_by_uri[uri] =
|
|
81
|
+
if relative
|
|
82
|
+
relative.split(File::SEPARATOR).any? { |segment| TEST_SEGMENTS.include?(segment) } ? TEST : CORE
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Package attribution is deliberately limited to documents Rubydex actually
|
|
88
|
+
# indexed, so a path that merely sits inside a package root cannot claim it.
|
|
89
|
+
#
|
|
90
|
+
#: (Rubydex::Location) -> Integer?
|
|
91
|
+
def package_index_for(location)
|
|
92
|
+
uri = location.uri
|
|
93
|
+
@package_index_by_uri.fetch(uri) do
|
|
94
|
+
path = path_for(uri)
|
|
95
|
+
@package_index_by_uri[uri] =
|
|
96
|
+
if path && @package_document_paths.include?(path)
|
|
97
|
+
@manifest.package_index_for(path)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -2,33 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
require "bundler"
|
|
4
4
|
require "bundler/lockfile_parser"
|
|
5
|
-
require "find"
|
|
6
5
|
require "pathname"
|
|
7
6
|
require "set"
|
|
8
7
|
require_relative "../dependency_warning"
|
|
8
|
+
require_relative "git_package_source"
|
|
9
9
|
|
|
10
10
|
module RubyLens
|
|
11
11
|
module Index
|
|
12
12
|
class Manifest
|
|
13
13
|
Package = Data.define(:name, :version, :role, :location, :root, :files)
|
|
14
14
|
DependencySystem = Data.define(:id, :package_indexes, :label_package_index)
|
|
15
|
-
GitPackagePaths = Data.define(:logical_root, :canonical_root, :logical_canonical_root)
|
|
16
15
|
|
|
17
|
-
class NixStoreProvider
|
|
18
|
-
STORE_ROOT = Pathname("/nix/store").freeze
|
|
19
|
-
STORE_OBJECT_PATTERN = /\A[0123456789abcdfghijklmnpqrsvwxyz]{32}-.+\z/
|
|
20
|
-
|
|
21
|
-
def trusted?(path)
|
|
22
|
-
path = Pathname(path).expand_path
|
|
23
|
-
return false unless Paths.inside?(path, STORE_ROOT)
|
|
24
|
-
|
|
25
|
-
first_component = path.relative_path_from(STORE_ROOT).each_filename.first
|
|
26
|
-
first_component && STORE_OBJECT_PATTERN.match?(first_component)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
UnsafeGitRequirePath = Class.new(StandardError)
|
|
31
|
-
UnsafeGitPackageFile = Class.new(StandardError)
|
|
32
16
|
GIT_SKIP_REASONS = DependencyWarning::REASONS
|
|
33
17
|
|
|
34
18
|
attr_reader :root, :files, :workspace_files, :packages, :dependency_systems, :warnings, :dependency_warnings
|
|
@@ -40,7 +24,7 @@ module RubyLens
|
|
|
40
24
|
def initialize(root:, lockfile: nil)
|
|
41
25
|
@root = Pathname(root).expand_path.realpath
|
|
42
26
|
@lockfile = Pathname(lockfile || @root.join("Gemfile.lock")).expand_path
|
|
43
|
-
@
|
|
27
|
+
@git_package_source = GitPackageSource.new(lockfile: @lockfile)
|
|
44
28
|
@warnings = []
|
|
45
29
|
@dependency_warnings = []
|
|
46
30
|
@packages = []
|
|
@@ -49,7 +33,7 @@ module RubyLens
|
|
|
49
33
|
@package_index_by_file = {}
|
|
50
34
|
@package_index_cache = {}
|
|
51
35
|
@relative_workspace_path_cache = {}
|
|
52
|
-
@
|
|
36
|
+
@workspace_path_cache = {}
|
|
53
37
|
end
|
|
54
38
|
|
|
55
39
|
def build
|
|
@@ -70,23 +54,27 @@ module RubyLens
|
|
|
70
54
|
|
|
71
55
|
def package_index_for(path)
|
|
72
56
|
path = path.to_s
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
57
|
+
@package_index_by_file.fetch(path) do
|
|
58
|
+
@package_index_cache.fetch(path) do
|
|
59
|
+
@package_index_cache[path] = uncached_package_index_for(path)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
77
62
|
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
78
63
|
nil
|
|
79
64
|
end
|
|
80
65
|
|
|
81
66
|
def workspace_path?(path)
|
|
82
|
-
|
|
67
|
+
path = path.to_s
|
|
68
|
+
@workspace_path_cache.fetch(path) do
|
|
69
|
+
@workspace_path_cache[path] = Paths.inside?(path, @root)
|
|
70
|
+
end
|
|
83
71
|
end
|
|
84
72
|
|
|
85
73
|
def relative_workspace_path(path)
|
|
86
74
|
path = path.to_s
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
75
|
+
@relative_workspace_path_cache.fetch(path) do
|
|
76
|
+
@relative_workspace_path_cache[path] = uncached_relative_workspace_path(path)
|
|
77
|
+
end
|
|
90
78
|
end
|
|
91
79
|
|
|
92
80
|
private
|
|
@@ -188,134 +176,17 @@ module RubyLens
|
|
|
188
176
|
end
|
|
189
177
|
|
|
190
178
|
def git_package(locked, role)
|
|
191
|
-
|
|
192
|
-
return skip_git_dependency(locked,
|
|
193
|
-
|
|
194
|
-
checkout = git_checkout_path(source)
|
|
195
|
-
return skip_git_dependency(locked, :checkout_unavailable) unless checkout.directory?
|
|
196
|
-
|
|
197
|
-
specification = (@git_spec_indexes[source] ||= git_specifications(source, checkout)).search(locked).first
|
|
198
|
-
return skip_git_dependency(locked, :specification_unavailable) unless specification
|
|
199
|
-
|
|
200
|
-
paths = git_package_paths(specification, checkout)
|
|
201
|
-
return skip_git_dependency(locked, :unsafe_specification_root) unless paths
|
|
202
|
-
|
|
203
|
-
files = git_indexable_files(paths, specification.require_paths)
|
|
179
|
+
resolution = @git_package_source.resolve(locked)
|
|
180
|
+
return skip_git_dependency(locked, resolution) if resolution.is_a?(Symbol)
|
|
204
181
|
|
|
205
182
|
Package.new(
|
|
206
183
|
name: locked.name,
|
|
207
184
|
version: locked.version.to_s,
|
|
208
185
|
role: role,
|
|
209
186
|
location: "external",
|
|
210
|
-
root:
|
|
211
|
-
files: files
|
|
187
|
+
root: resolution.root,
|
|
188
|
+
files: resolution.files,
|
|
212
189
|
)
|
|
213
|
-
rescue UnsafeGitRequirePath
|
|
214
|
-
skip_git_dependency(locked, :unsafe_require_paths)
|
|
215
|
-
rescue UnsafeGitPackageFile
|
|
216
|
-
skip_git_dependency(locked, :unsafe_package_files)
|
|
217
|
-
rescue Bundler::BundlerError, Gem::Exception
|
|
218
|
-
skip_git_dependency(locked, :specification_unreadable)
|
|
219
|
-
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
220
|
-
skip_git_dependency(locked, :checkout_unavailable)
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def git_checkout_path(source)
|
|
224
|
-
bundle_root = @lockfile.dirname
|
|
225
|
-
app_config = ENV["BUNDLE_APP_CONFIG"]
|
|
226
|
-
app_config_path = if app_config
|
|
227
|
-
Pathname(app_config).expand_path(bundle_root)
|
|
228
|
-
else
|
|
229
|
-
bundle_root.join(".bundle")
|
|
230
|
-
end
|
|
231
|
-
bundle_path = Pathname(Bundler::Settings.new(app_config_path).path.path).expand_path(bundle_root)
|
|
232
|
-
bundle_path.join("bundler/gems", source.extension_dir_name)
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def git_specifications(source, checkout)
|
|
236
|
-
source.__send__(:set_install_path!, checkout)
|
|
237
|
-
source.specs
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def git_package_paths(specification, checkout)
|
|
241
|
-
logical_root = Pathname(specification.full_gem_path).expand_path
|
|
242
|
-
logical_gemspec = Pathname(specification.loaded_from).expand_path
|
|
243
|
-
return unless Paths.inside?(logical_root, checkout)
|
|
244
|
-
return unless logical_gemspec.dirname == logical_root
|
|
245
|
-
return unless logical_gemspec.file? && logical_root.directory?
|
|
246
|
-
|
|
247
|
-
canonical_checkout = checkout.realpath
|
|
248
|
-
resolved_gemspec_root = logical_gemspec.realpath.dirname
|
|
249
|
-
logical_canonical_root = logical_root.realpath
|
|
250
|
-
return unless resolved_gemspec_root.directory?
|
|
251
|
-
normal_checkout = !checkout.symlink? &&
|
|
252
|
-
Paths.inside?(resolved_gemspec_root, canonical_checkout) &&
|
|
253
|
-
Paths.inside?(logical_canonical_root, canonical_checkout)
|
|
254
|
-
return unless normal_checkout || @nix_store_provider.trusted?(resolved_gemspec_root)
|
|
255
|
-
|
|
256
|
-
canonical_root = normal_checkout ? logical_canonical_root : resolved_gemspec_root
|
|
257
|
-
|
|
258
|
-
GitPackagePaths.new(
|
|
259
|
-
logical_root: logical_root,
|
|
260
|
-
canonical_root: canonical_root,
|
|
261
|
-
logical_canonical_root: logical_canonical_root,
|
|
262
|
-
)
|
|
263
|
-
rescue TypeError, ArgumentError
|
|
264
|
-
nil
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def git_indexable_files(paths, require_paths)
|
|
268
|
-
logical_require_paths = require_paths.map do |relative_path|
|
|
269
|
-
raise UnsafeGitRequirePath unless relative_path.is_a?(String)
|
|
270
|
-
|
|
271
|
-
relative = Pathname(relative_path)
|
|
272
|
-
raise UnsafeGitRequirePath if relative.absolute? || relative.each_filename.include?("..")
|
|
273
|
-
|
|
274
|
-
candidate = paths.logical_root.join(relative).cleanpath
|
|
275
|
-
raise UnsafeGitRequirePath unless Paths.inside?(candidate, paths.logical_root)
|
|
276
|
-
|
|
277
|
-
candidate
|
|
278
|
-
rescue ArgumentError, EncodingError
|
|
279
|
-
raise UnsafeGitRequirePath
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
files = []
|
|
283
|
-
visited_directories = Set.new
|
|
284
|
-
logical_require_paths.each do |path|
|
|
285
|
-
next unless path.exist? || path.symlink?
|
|
286
|
-
|
|
287
|
-
traverse_git_package_path(path, paths, files, visited_directories, Set.new)
|
|
288
|
-
end
|
|
289
|
-
files.uniq.sort
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def traverse_git_package_path(path, paths, files, visited_directories, active_directories)
|
|
293
|
-
resolved = path.realpath
|
|
294
|
-
if resolved.file?
|
|
295
|
-
raise UnsafeGitPackageFile unless Paths.inside?(resolved, paths.canonical_root)
|
|
296
|
-
|
|
297
|
-
files << resolved.to_s if indexable?(path)
|
|
298
|
-
return
|
|
299
|
-
end
|
|
300
|
-
return unless resolved.directory?
|
|
301
|
-
|
|
302
|
-
unless Paths.inside?(resolved, paths.logical_canonical_root) || Paths.inside?(resolved, paths.canonical_root)
|
|
303
|
-
raise UnsafeGitPackageFile
|
|
304
|
-
end
|
|
305
|
-
raise UnsafeGitPackageFile if active_directories.include?(resolved)
|
|
306
|
-
return if visited_directories.include?(resolved)
|
|
307
|
-
|
|
308
|
-
active_directories.add(resolved)
|
|
309
|
-
begin
|
|
310
|
-
path.children.sort_by(&:to_s).each do |child|
|
|
311
|
-
traverse_git_package_path(child, paths, files, visited_directories, active_directories)
|
|
312
|
-
end
|
|
313
|
-
visited_directories.add(resolved)
|
|
314
|
-
ensure
|
|
315
|
-
active_directories.delete(resolved)
|
|
316
|
-
end
|
|
317
|
-
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
318
|
-
raise UnsafeGitPackageFile
|
|
319
190
|
end
|
|
320
191
|
|
|
321
192
|
def skip_git_dependency(locked, reason_code)
|
|
@@ -428,20 +299,54 @@ module RubyLens
|
|
|
428
299
|
end.flat_map { |path| enumerate(path, root) }.uniq.sort
|
|
429
300
|
end
|
|
430
301
|
|
|
302
|
+
# Walks an already-canonical directory (the caller resolved and containment-checked
|
|
303
|
+
# it) without re-resolving every entry: descending only non-symlink directories
|
|
304
|
+
# keeps each joined path canonical, so regular files join the list with a cheap
|
|
305
|
+
# prefix containment check and only symlinked entries pay for a realpath. Symlinked
|
|
306
|
+
# directories are never descended, matching Find.find's lstat-based traversal.
|
|
431
307
|
def enumerate(path, root)
|
|
432
308
|
return [path.to_s] if path.file? && indexable?(path) && Paths.inside?(path.realpath, root)
|
|
433
309
|
return [] unless path.directory?
|
|
310
|
+
return [] if path.symlink?
|
|
434
311
|
|
|
435
312
|
files = []
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
313
|
+
root_prefix = "#{root}#{File::SEPARATOR}"
|
|
314
|
+
start = path.to_s
|
|
315
|
+
# Child names must come back in the starting path's encoding. A package
|
|
316
|
+
# root that is not valid UTF-8 otherwise yields UTF-8 entry names that
|
|
317
|
+
# cannot be joined to it, and the whole build aborts on a tree the
|
|
318
|
+
# Find-based walk indexed fine.
|
|
319
|
+
encoding = start.encoding == Encoding::US_ASCII ? Encoding.find("filesystem") : start.encoding
|
|
320
|
+
pending = [start]
|
|
321
|
+
while (directory = pending.pop)
|
|
322
|
+
entries = begin
|
|
323
|
+
Dir.children(directory, encoding: encoding)
|
|
324
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG, Errno::EINVAL
|
|
325
|
+
next
|
|
326
|
+
end
|
|
327
|
+
entries.each do |entry|
|
|
328
|
+
candidate = File.join(directory, entry)
|
|
329
|
+
stat = begin
|
|
330
|
+
File.lstat(candidate)
|
|
331
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG, Errno::EINVAL
|
|
332
|
+
next
|
|
333
|
+
end
|
|
334
|
+
if stat.symlink?
|
|
335
|
+
next if File.directory?(candidate)
|
|
336
|
+
next unless INDEXABLE_EXTENSIONS.include?(File.extname(entry))
|
|
337
|
+
|
|
338
|
+
begin
|
|
339
|
+
resolved = File.realpath(candidate)
|
|
340
|
+
files << resolved if Paths.inside?(resolved, root)
|
|
341
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
342
|
+
next
|
|
343
|
+
end
|
|
344
|
+
elsif stat.directory?
|
|
345
|
+
pending << candidate
|
|
346
|
+
elsif INDEXABLE_EXTENSIONS.include?(File.extname(entry))
|
|
347
|
+
files << candidate if candidate.start_with?(root_prefix)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
445
350
|
end
|
|
446
351
|
files
|
|
447
352
|
end
|
|
@@ -11,28 +11,24 @@ module RubyLens
|
|
|
11
11
|
SPEC_SEGMENTS = Set.new(%w[spec specs]).freeze
|
|
12
12
|
PROXY_NAME_PREFIX = "RSpec example group #"
|
|
13
13
|
|
|
14
|
-
Group = Data.define(:name, :component)
|
|
15
14
|
Result = Data.define(:groups, :method_count)
|
|
16
15
|
|
|
17
|
-
def initialize(graph:, manifest:, package_document_paths: Set.new)
|
|
16
|
+
def initialize(graph:, manifest:, package_document_paths: Set.new, documents_with_paths: nil)
|
|
18
17
|
@graph = graph
|
|
19
18
|
@manifest = manifest
|
|
20
19
|
@package_document_paths = package_document_paths
|
|
20
|
+
@documents_with_paths = documents_with_paths
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def call
|
|
24
24
|
groups = []
|
|
25
25
|
method_count = 0
|
|
26
26
|
|
|
27
|
-
spec_documents.each do |document,
|
|
27
|
+
spec_documents.each do |document, _relative|
|
|
28
28
|
group_count, example_count = reference_counts(document.method_references)
|
|
29
29
|
method_count += example_count
|
|
30
|
-
component = SourcePath.component_for(relative)
|
|
31
30
|
group_count.times do
|
|
32
|
-
groups <<
|
|
33
|
-
format("%s%06d", PROXY_NAME_PREFIX, groups.length + 1),
|
|
34
|
-
component,
|
|
35
|
-
)
|
|
31
|
+
groups << format("%s%06d", PROXY_NAME_PREFIX, groups.length + 1)
|
|
36
32
|
end
|
|
37
33
|
end
|
|
38
34
|
|
|
@@ -41,10 +37,16 @@ module RubyLens
|
|
|
41
37
|
|
|
42
38
|
private
|
|
43
39
|
|
|
40
|
+
# The adapter passes its already-resolved [document, path] pairs so the
|
|
41
|
+
# documents are not re-enumerated and their URIs not re-parsed here;
|
|
42
|
+
# standalone construction still resolves them from the graph.
|
|
44
43
|
def spec_documents
|
|
45
|
-
@graph.documents.filter_map do |document|
|
|
44
|
+
pairs = @documents_with_paths || @graph.documents.filter_map do |document|
|
|
46
45
|
path = SourcePath.from_file_uri(document.uri)
|
|
47
|
-
|
|
46
|
+
[document, path] if path
|
|
47
|
+
end
|
|
48
|
+
pairs.filter_map do |document, path|
|
|
49
|
+
next unless @manifest.workspace_path?(path)
|
|
48
50
|
next if @package_document_paths.include?(path)
|
|
49
51
|
|
|
50
52
|
relative = @manifest.relative_workspace_path(path)
|