audition 0.2.4 → 0.4.0
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 +175 -39
- data/lib/audition/bundle_sweep.rb +32 -15
- data/lib/audition/cli.rb +181 -100
- data/lib/audition/config.rb +33 -7
- data/lib/audition/dynamic/harness.rb +323 -31
- data/lib/audition/dynamic/prober.rb +190 -43
- data/lib/audition/finding.rb +10 -2
- data/lib/audition/progress.rb +418 -0
- data/lib/audition/report/github.rb +69 -0
- data/lib/audition/report/json.rb +68 -0
- data/lib/audition/report/style.rb +74 -0
- data/lib/audition/report/sweep.rb +182 -0
- data/lib/audition/report/text.rb +160 -0
- data/lib/audition/report.rb +16 -295
- data/lib/audition/rewriters.rb +8 -5
- data/lib/audition/static/analyzer.rb +86 -17
- data/lib/audition/static/checks/dependency_class_state.rb +160 -0
- data/lib/audition/static/checks/mutable_constants.rb +162 -28
- data/lib/audition/static/checks/runtime_require.rb +4 -1
- data/lib/audition/static/checks/unsafe_calls.rb +7 -6
- data/lib/audition/static/checks/unshareable_reads.rb +259 -0
- data/lib/audition/static/checks.rb +5 -2
- data/lib/audition/static/gem_calls.rb +1770 -0
- data/lib/audition/static/graph_audit.rb +1053 -12
- data/lib/audition/static/literal_classifier.rb +191 -20
- data/lib/audition/static/native_extensions.rb +175 -0
- data/lib/audition/static/source_file.rb +70 -0
- data/lib/audition/static/work_split.rb +54 -0
- data/lib/audition/target.rb +147 -14
- data/lib/audition/version.rb +1 -1
- data/lib/audition.rb +3 -0
- metadata +15 -4
data/lib/audition/target.rb
CHANGED
|
@@ -12,6 +12,25 @@ module Audition
|
|
|
12
12
|
vendor node_modules tmp log coverage pkg .git .bundle
|
|
13
13
|
].freeze
|
|
14
14
|
|
|
15
|
+
# Directories holding the target's tests rather than the code
|
|
16
|
+
# a production boot loads. The default; `test_dirs` in
|
|
17
|
+
# .audition.yml replaces it for a project that files them
|
|
18
|
+
# elsewhere.
|
|
19
|
+
TEST_DIRS = %w[test spec features].freeze
|
|
20
|
+
|
|
21
|
+
# Cargo and Zig build output: full of .so/.dylib artifacts that
|
|
22
|
+
# are not the extension `require` loads.
|
|
23
|
+
BUILD_DIRS = %w[target zig-out].freeze
|
|
24
|
+
|
|
25
|
+
# What Ruby loads as a native extension (RbConfig DLEXT is
|
|
26
|
+
# "bundle" on macOS, "so" everywhere else, Windows included).
|
|
27
|
+
COMPILED = "*.{bundle,so}"
|
|
28
|
+
|
|
29
|
+
# Generated type stubs. The extension is the convention; where
|
|
30
|
+
# the generator writes them is not, so they are found by
|
|
31
|
+
# extension anywhere in the tree.
|
|
32
|
+
STUBS = "*.rbi"
|
|
33
|
+
|
|
15
34
|
# @return [Symbol] one of `:script`, `:gem`, `:rack`, `:rails`,
|
|
16
35
|
# `:directory`, `:bundle`
|
|
17
36
|
attr_reader :type
|
|
@@ -22,6 +41,14 @@ module Audition
|
|
|
22
41
|
# @return [Array<String>] Ruby files to scan statically
|
|
23
42
|
attr_reader :ruby_files
|
|
24
43
|
|
|
44
|
+
# @return [Array<String>] compiled extension files (.bundle/.so)
|
|
45
|
+
# shipped with the target; empty for scripts and file lists
|
|
46
|
+
attr_reader :compiled_files
|
|
47
|
+
|
|
48
|
+
# @return [Array<String>] generated type stubs (.rbi) found
|
|
49
|
+
# anywhere in the target's tree
|
|
50
|
+
attr_reader :stub_files
|
|
51
|
+
|
|
25
52
|
# @return [Hash, nil] dynamic probe entry (`:mode` plus
|
|
26
53
|
# mode-specific keys), nil for static-only targets
|
|
27
54
|
attr_reader :entry
|
|
@@ -43,6 +70,31 @@ module Audition
|
|
|
43
70
|
end
|
|
44
71
|
end
|
|
45
72
|
|
|
73
|
+
# Builds a static-only target from an explicit file list, the
|
|
74
|
+
# shape git hooks hand over (lefthook's {staged_files},
|
|
75
|
+
# pre-commit's filename arguments). Config, pragmas, and the
|
|
76
|
+
# baseline resolve against the working directory, which is the
|
|
77
|
+
# repository root when a hook manager runs the command.
|
|
78
|
+
#
|
|
79
|
+
# @param paths [Array<String>] `.rb`/`.ru` files
|
|
80
|
+
# @return [Target] type `:files`, no dynamic entry
|
|
81
|
+
# @raise [Audition::Error] when a path is not a Ruby file
|
|
82
|
+
def self.for_files(paths)
|
|
83
|
+
paths.each do |path|
|
|
84
|
+
unless File.file?(path) && path.end_with?(".rb", ".ru")
|
|
85
|
+
raise Error, "#{path} is not a Ruby file"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
new(
|
|
90
|
+
type: :files,
|
|
91
|
+
root: Dir.pwd,
|
|
92
|
+
ruby_files: paths,
|
|
93
|
+
entry: nil,
|
|
94
|
+
stub_files: stubs(Dir.pwd)
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
46
98
|
def self.from_file(path)
|
|
47
99
|
if File.basename(path) == "Gemfile.lock"
|
|
48
100
|
return new(
|
|
@@ -60,10 +112,26 @@ module Audition
|
|
|
60
112
|
type: :script,
|
|
61
113
|
root: File.dirname(path),
|
|
62
114
|
ruby_files: [path],
|
|
63
|
-
|
|
115
|
+
# A file inside a Rails root cannot run standalone; the
|
|
116
|
+
# script probe would report a misleading boot failure, so
|
|
117
|
+
# such files stay static-only.
|
|
118
|
+
entry: rails_member?(path) ? nil : {mode: :script, path: path},
|
|
119
|
+
stub_files: stubs(File.dirname(path))
|
|
64
120
|
)
|
|
65
121
|
end
|
|
66
122
|
|
|
123
|
+
def self.rails_member?(path)
|
|
124
|
+
dir = File.dirname(File.expand_path(path))
|
|
125
|
+
until dir == (parent = File.dirname(dir))
|
|
126
|
+
return true if File.file?(
|
|
127
|
+
File.join(dir, "config", "application.rb")
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
dir = parent
|
|
131
|
+
end
|
|
132
|
+
false
|
|
133
|
+
end
|
|
134
|
+
|
|
67
135
|
def self.from_directory(dir)
|
|
68
136
|
application_rb = File.join(dir, "config", "application.rb")
|
|
69
137
|
config_ru = File.join(dir, "config.ru")
|
|
@@ -80,7 +148,9 @@ module Audition
|
|
|
80
148
|
type: :directory,
|
|
81
149
|
root: dir,
|
|
82
150
|
ruby_files: glob(dir),
|
|
83
|
-
entry: nil
|
|
151
|
+
entry: nil,
|
|
152
|
+
compiled_files: compiled(dir),
|
|
153
|
+
stub_files: stubs(dir)
|
|
84
154
|
)
|
|
85
155
|
end
|
|
86
156
|
end
|
|
@@ -94,15 +164,21 @@ module Audition
|
|
|
94
164
|
glob(File.join(spec.full_gem_path, rp))
|
|
95
165
|
end,
|
|
96
166
|
entry: {mode: :require, feature: name,
|
|
97
|
-
|
|
167
|
+
load_paths: spec.full_require_paths,
|
|
168
|
+
root: spec.full_gem_path},
|
|
169
|
+
compiled_files: compiled_for(spec),
|
|
170
|
+
stub_files: stubs(spec.full_gem_path)
|
|
98
171
|
)
|
|
99
172
|
rescue Gem::MissingSpecError
|
|
100
173
|
raise Error,
|
|
101
174
|
"#{name} is not a file, directory, or installed gem"
|
|
102
175
|
end
|
|
103
176
|
|
|
177
|
+
# An app's own Ruby is not confined to app/lib/config: local
|
|
178
|
+
# gems, engines and tests boot into the same process, so the
|
|
179
|
+
# whole root is scanned.
|
|
104
180
|
def self.rails_target(dir)
|
|
105
|
-
files =
|
|
181
|
+
files = glob(dir)
|
|
106
182
|
files << File.join(dir, "config.ru")
|
|
107
183
|
new(
|
|
108
184
|
type: :rails,
|
|
@@ -112,7 +188,9 @@ module Audition
|
|
|
112
188
|
mode: :rails,
|
|
113
189
|
environment: File.join(dir, "config", "environment.rb"),
|
|
114
190
|
root: dir
|
|
115
|
-
}
|
|
191
|
+
},
|
|
192
|
+
compiled_files: compiled(dir),
|
|
193
|
+
stub_files: stubs(dir)
|
|
116
194
|
)
|
|
117
195
|
end
|
|
118
196
|
|
|
@@ -121,22 +199,31 @@ module Audition
|
|
|
121
199
|
type: :rack,
|
|
122
200
|
root: dir,
|
|
123
201
|
ruby_files: [config_ru] + glob(dir),
|
|
124
|
-
entry: {mode: :rack, config_ru: config_ru}
|
|
202
|
+
entry: {mode: :rack, config_ru: config_ru, root: dir},
|
|
203
|
+
compiled_files: compiled(dir),
|
|
204
|
+
stub_files: stubs(dir)
|
|
125
205
|
)
|
|
126
206
|
end
|
|
127
207
|
|
|
208
|
+
# Reading require_paths off the gemspec would mean evaluating
|
|
209
|
+
# it, which the static pass never does; `lib` is the packaging
|
|
210
|
+
# default, and a gem that keeps its code elsewhere falls back to
|
|
211
|
+
# the whole checkout rather than scanning nothing.
|
|
128
212
|
def self.gem_dir_target(dir, gemspec)
|
|
129
213
|
lib = File.join(dir, "lib")
|
|
214
|
+
paths = File.directory?(lib) ? [lib] : [dir]
|
|
130
215
|
new(
|
|
131
216
|
type: :gem,
|
|
132
217
|
root: dir,
|
|
133
|
-
ruby_files: glob(
|
|
218
|
+
ruby_files: paths.flat_map { |path| glob(path) },
|
|
134
219
|
entry: {
|
|
135
220
|
mode: :require,
|
|
136
221
|
feature: File.basename(gemspec, ".gemspec"),
|
|
137
|
-
load_paths:
|
|
222
|
+
load_paths: paths,
|
|
138
223
|
root: dir
|
|
139
|
-
}
|
|
224
|
+
},
|
|
225
|
+
compiled_files: compiled(dir),
|
|
226
|
+
stub_files: stubs(dir)
|
|
140
227
|
)
|
|
141
228
|
end
|
|
142
229
|
|
|
@@ -149,24 +236,70 @@ module Audition
|
|
|
149
236
|
raw.sub(%r{/+\z}, "")
|
|
150
237
|
end
|
|
151
238
|
|
|
152
|
-
def self.glob(dir)
|
|
239
|
+
def self.glob(dir, pattern = "*.rb", skip: EXCLUDED_DIRS)
|
|
153
240
|
dir = normalize(dir)
|
|
154
|
-
Dir[File.join(dir, "**",
|
|
241
|
+
Dir[File.join(dir, "**", pattern)].reject do |path|
|
|
155
242
|
relative = path.delete_prefix("#{dir}/")
|
|
156
243
|
parts = relative.split("/")
|
|
157
|
-
parts.any? { |p|
|
|
244
|
+
parts.any? { |p| skip.include?(p) || p.start_with?(".") }
|
|
158
245
|
end.sort
|
|
159
246
|
end
|
|
160
247
|
|
|
248
|
+
# Generated type stubs anywhere under a directory.
|
|
249
|
+
#
|
|
250
|
+
# @param dir [String]
|
|
251
|
+
# @return [Array<String>]
|
|
252
|
+
def self.stubs(dir)
|
|
253
|
+
glob(dir, STUBS)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# macOS debug-symbol bundles (x.bundle.dSYM/...) carry a file
|
|
257
|
+
# with the extension's name that nothing ever loads.
|
|
258
|
+
def self.compiled(dir)
|
|
259
|
+
glob(dir, COMPILED, skip: EXCLUDED_DIRS + BUILD_DIRS).reject do |p|
|
|
260
|
+
p.split("/").any? { |part| part.end_with?(".dSYM") }
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Compiled extension files of an installed gem. RubyGems builds
|
|
265
|
+
# a source gem into its extension dir and also copies the result
|
|
266
|
+
# under lib/, so the same file shows up on two require paths;
|
|
267
|
+
# keep one per require-relative name.
|
|
268
|
+
#
|
|
269
|
+
# @param spec [Gem::Specification]
|
|
270
|
+
# @return [Array<String>]
|
|
271
|
+
def self.compiled_for(spec)
|
|
272
|
+
spec.full_require_paths.flat_map do |rp|
|
|
273
|
+
compiled(rp).map { |path| [path.delete_prefix("#{rp}/"), path] }
|
|
274
|
+
end.uniq(&:first).map(&:last)
|
|
275
|
+
end
|
|
276
|
+
|
|
161
277
|
private_class_method :from_file, :from_directory, :from_gem_name,
|
|
162
278
|
:rails_target, :rack_target, :gem_dir_target,
|
|
163
|
-
:glob, :normalize
|
|
279
|
+
:rails_member?, :glob, :compiled, :normalize
|
|
164
280
|
|
|
165
|
-
def initialize(type:, root:, ruby_files:, entry
|
|
281
|
+
def initialize(type:, root:, ruby_files:, entry:,
|
|
282
|
+
compiled_files: [], stub_files: [])
|
|
166
283
|
@type = type
|
|
167
284
|
@root = root
|
|
168
285
|
@ruby_files = ruby_files
|
|
169
286
|
@entry = entry
|
|
287
|
+
@compiled_files = compiled_files
|
|
288
|
+
@stub_files = stub_files
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Whether a path is test/spec code rather than code the
|
|
292
|
+
# production boot loads.
|
|
293
|
+
#
|
|
294
|
+
# @param path [String] absolute or root-relative
|
|
295
|
+
# @param dirs [Array<String>] directory names that hold tests
|
|
296
|
+
# @return [Boolean]
|
|
297
|
+
def test_file?(path, dirs: TEST_DIRS)
|
|
298
|
+
relative = File.expand_path(path, root)
|
|
299
|
+
.delete_prefix("#{root}/")
|
|
300
|
+
parts = relative.split("/")
|
|
301
|
+
parts[0..-2].any? { |p| dirs.include?(p) } ||
|
|
302
|
+
parts.last.to_s.end_with?("_test.rb", "_spec.rb")
|
|
170
303
|
end
|
|
171
304
|
end
|
|
172
305
|
end
|
data/lib/audition/version.rb
CHANGED
data/lib/audition.rb
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "audition/version"
|
|
4
|
+
require_relative "audition/progress"
|
|
4
5
|
require_relative "audition/finding"
|
|
5
6
|
require_relative "audition/target"
|
|
6
7
|
require_relative "audition/static/source_file"
|
|
7
8
|
require_relative "audition/static/literal_classifier"
|
|
8
9
|
require_relative "audition/static/checks"
|
|
9
10
|
require_relative "audition/static/graph_audit"
|
|
11
|
+
require_relative "audition/static/gem_calls"
|
|
12
|
+
require_relative "audition/static/native_extensions"
|
|
10
13
|
require_relative "audition/static/analyzer"
|
|
11
14
|
require_relative "audition/dynamic/prober"
|
|
12
15
|
require_relative "audition/report"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: audition
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Markin
|
|
@@ -43,14 +43,14 @@ dependencies:
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0.
|
|
46
|
+
version: '0.4'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0.
|
|
53
|
+
version: '0.4'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: table_tennis
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -102,19 +102,30 @@ files:
|
|
|
102
102
|
- lib/audition/dynamic/prober.rb
|
|
103
103
|
- lib/audition/finding.rb
|
|
104
104
|
- lib/audition/fixer.rb
|
|
105
|
+
- lib/audition/progress.rb
|
|
105
106
|
- lib/audition/report.rb
|
|
107
|
+
- lib/audition/report/github.rb
|
|
108
|
+
- lib/audition/report/json.rb
|
|
109
|
+
- lib/audition/report/style.rb
|
|
110
|
+
- lib/audition/report/sweep.rb
|
|
111
|
+
- lib/audition/report/text.rb
|
|
106
112
|
- lib/audition/rewriters.rb
|
|
107
113
|
- lib/audition/static/analyzer.rb
|
|
108
114
|
- lib/audition/static/checks.rb
|
|
109
115
|
- lib/audition/static/checks/base.rb
|
|
116
|
+
- lib/audition/static/checks/dependency_class_state.rb
|
|
110
117
|
- lib/audition/static/checks/global_variables.rb
|
|
111
118
|
- lib/audition/static/checks/mutable_constants.rb
|
|
112
119
|
- lib/audition/static/checks/ractor_isolation.rb
|
|
113
120
|
- lib/audition/static/checks/runtime_require.rb
|
|
114
121
|
- lib/audition/static/checks/unsafe_calls.rb
|
|
122
|
+
- lib/audition/static/checks/unshareable_reads.rb
|
|
123
|
+
- lib/audition/static/gem_calls.rb
|
|
115
124
|
- lib/audition/static/graph_audit.rb
|
|
116
125
|
- lib/audition/static/literal_classifier.rb
|
|
126
|
+
- lib/audition/static/native_extensions.rb
|
|
117
127
|
- lib/audition/static/source_file.rb
|
|
128
|
+
- lib/audition/static/work_split.rb
|
|
118
129
|
- lib/audition/target.rb
|
|
119
130
|
- lib/audition/version.rb
|
|
120
131
|
homepage: https://github.com/yaroslav/audition
|
|
@@ -137,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
137
148
|
- !ruby/object:Gem::Version
|
|
138
149
|
version: '0'
|
|
139
150
|
requirements: []
|
|
140
|
-
rubygems_version: 4.0.
|
|
151
|
+
rubygems_version: 4.0.20
|
|
141
152
|
specification_version: 4
|
|
142
153
|
summary: Probe Ruby code for Ractor-readiness
|
|
143
154
|
test_files: []
|