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.
@@ -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
- entry: {mode: :script, path: path}
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
- root: spec.full_gem_path}
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 = %w[app lib config].flat_map { |d| glob(File.join(dir, d)) }
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(lib),
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: [lib],
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, "**", "*.rb")].reject do |path|
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| EXCLUDED_DIRS.include?(p) || p.start_with?(".") }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Audition
4
- VERSION = "0.2.4"
4
+ VERSION = "0.4.0"
5
5
  end
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.2.4
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.2'
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.2'
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.16
151
+ rubygems_version: 4.0.20
141
152
  specification_version: 4
142
153
  summary: Probe Ruby code for Ractor-readiness
143
154
  test_files: []