semverve 0.2.1 → 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.
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+ require_relative "version_checks"
5
+
6
+ module Semverve
7
+ ##
8
+ # Framework-specific configuration adapters.
9
+ module Adapters
10
+ class << self
11
+ ##
12
+ # Registers a framework adapter.
13
+ #
14
+ # @param [#name, #defaults, #checks] adapter
15
+ #
16
+ # @return [#name, #defaults, #checks]
17
+ def register(adapter)
18
+ adapters[adapter.name] = adapter
19
+ end
20
+
21
+ ##
22
+ # Returns an adapter by name.
23
+ #
24
+ # @param [Symbol, String] name
25
+ #
26
+ # @return [#name, #defaults, #checks]
27
+ def fetch(name)
28
+ normalized_name = normalize_name(name)
29
+ adapter = adapters[normalized_name]
30
+ return adapter if adapter
31
+
32
+ raise Error, unknown_adapter_message(name)
33
+ end
34
+
35
+ ##
36
+ # Defaults for the configured adapter.
37
+ #
38
+ # @param [Symbol, String, nil] name
39
+ # @param [Semverve::Configuration] configuration
40
+ #
41
+ # @return [Hash]
42
+ def defaults_for(name, configuration)
43
+ return {} unless name
44
+
45
+ fetch(name).defaults(configuration)
46
+ end
47
+
48
+ ##
49
+ # Checks supplied by all registered adapters.
50
+ #
51
+ # @return [Array<#name>]
52
+ def checks
53
+ adapters.values.flat_map(&:checks).each_with_object({}) do |check, checks|
54
+ checks[check.name] = check
55
+ end.values
56
+ end
57
+
58
+ ##
59
+ # Checks supplied by the configured adapter.
60
+ #
61
+ # @param [Symbol, String, nil] name
62
+ #
63
+ # @return [Array<#name>]
64
+ def checks_for(name)
65
+ return [] unless name
66
+
67
+ fetch(name).checks
68
+ end
69
+
70
+ ##
71
+ # Whether package names should be inferred for the configured adapter.
72
+ #
73
+ # @param [Symbol, String, nil] name
74
+ #
75
+ # @return [Boolean]
76
+ def infer_package_name?(name)
77
+ return true unless name
78
+
79
+ fetch(name).infer_package_name?
80
+ end
81
+
82
+ ##
83
+ # Registered adapter names.
84
+ #
85
+ # @return [Array<Symbol>]
86
+ def names
87
+ adapters.keys
88
+ end
89
+
90
+ private
91
+
92
+ ##
93
+ # Adapter registry.
94
+ #
95
+ # @return [Hash<Symbol, #name>]
96
+ def adapters
97
+ @adapters ||= {}
98
+ end
99
+
100
+ ##
101
+ # Normalizes an adapter name.
102
+ #
103
+ # @param [Object] name
104
+ #
105
+ # @return [Symbol, Object]
106
+ def normalize_name(name)
107
+ name.respond_to?(:to_sym) ? name.to_sym : name
108
+ end
109
+
110
+ ##
111
+ # Error message for invalid adapters.
112
+ #
113
+ # @param [Object] name
114
+ #
115
+ # @return [String]
116
+ def unknown_adapter_message(name)
117
+ adapter_names = names.map(&:inspect)
118
+ valid_adapters = "#{adapter_names[0...-1].join(", ")}, or #{adapter_names.last}"
119
+ "Unknown adapter #{name.inspect}. Use #{valid_adapters}."
120
+ end
121
+ end
122
+
123
+ ##
124
+ # Shared adapter helpers.
125
+ class Base
126
+ ##
127
+ # Framework-specific checks.
128
+ #
129
+ # @return [Array<#name>]
130
+ def checks
131
+ []
132
+ end
133
+
134
+ ##
135
+ # Whether Semverve should infer package names from version files or gemspecs.
136
+ #
137
+ # @return [Boolean]
138
+ def infer_package_name?
139
+ true
140
+ end
141
+
142
+ private
143
+
144
+ ##
145
+ # Module name fallback based on the project directory.
146
+ #
147
+ # @param [String] root
148
+ #
149
+ # @return [String]
150
+ def project_module_name(root)
151
+ camelize(File.basename(File.expand_path(root)))
152
+ end
153
+
154
+ ##
155
+ # Converts a project directory into a Ruby module name.
156
+ #
157
+ # @param [String] value
158
+ #
159
+ # @return [String]
160
+ def camelize(value)
161
+ value.split(/[_-]/).map(&:capitalize).join
162
+ end
163
+ end
164
+
165
+ ##
166
+ # Rails application adapter.
167
+ class Rails < Base
168
+ ##
169
+ # Adapter name.
170
+ #
171
+ # @return [Symbol]
172
+ def name
173
+ :rails
174
+ end
175
+
176
+ ##
177
+ # Rails-specific Semverve defaults.
178
+ #
179
+ # @param [Semverve::Configuration] _configuration
180
+ #
181
+ # @return [Hash]
182
+ def defaults(_configuration)
183
+ root = rails_root || Dir.pwd
184
+
185
+ {
186
+ format: :simple,
187
+ module_name: rails_module_name || project_module_name(root),
188
+ root: root,
189
+ version_checks: [:doc_references, :code_references, :rails_config_metadata],
190
+ version_file: File.join("config", "version.rb")
191
+ }
192
+ end
193
+
194
+ ##
195
+ # Rails-specific checks.
196
+ #
197
+ # @return [Array<#name>]
198
+ def checks
199
+ [VersionChecks::RailsConfigMetadataCheck.new]
200
+ end
201
+
202
+ ##
203
+ # Rails apps should not infer a package name from config/version.rb.
204
+ #
205
+ # @return [Boolean]
206
+ def infer_package_name?
207
+ false
208
+ end
209
+
210
+ private
211
+
212
+ ##
213
+ # Rails.root when Rails is available.
214
+ #
215
+ # @return [String, nil]
216
+ def rails_root
217
+ return unless rails_defined?
218
+ return unless ::Rails.respond_to?(:root)
219
+ return unless ::Rails.root
220
+
221
+ ::Rails.root.to_s
222
+ end
223
+
224
+ ##
225
+ # Application module name inferred from Rails.application.
226
+ #
227
+ # @return [String, nil]
228
+ def rails_module_name
229
+ return unless rails_defined?
230
+ return unless ::Rails.respond_to?(:application)
231
+ return unless ::Rails.application
232
+
233
+ application_class = ::Rails.application.class
234
+ return application_class.module_parent_name if application_class.respond_to?(:module_parent_name)
235
+
236
+ class_name = application_class.name
237
+ return unless class_name&.include?("::")
238
+
239
+ class_name.split("::").first
240
+ end
241
+
242
+ ##
243
+ # Whether Rails is loaded.
244
+ #
245
+ # @return [Boolean]
246
+ def rails_defined?
247
+ Object.const_defined?(:Rails)
248
+ end
249
+ end
250
+
251
+ ##
252
+ # Sinatra application adapter.
253
+ class Sinatra < Base
254
+ ##
255
+ # Adapter name.
256
+ #
257
+ # @return [Symbol]
258
+ def name
259
+ :sinatra
260
+ end
261
+
262
+ ##
263
+ # Sinatra-specific Semverve defaults.
264
+ #
265
+ # @param [Semverve::Configuration] _configuration
266
+ #
267
+ # @return [Hash]
268
+ def defaults(_configuration)
269
+ root = Dir.pwd
270
+
271
+ {
272
+ format: :simple,
273
+ module_name: project_module_name(root),
274
+ root: root,
275
+ version_checks: [:doc_references, :code_references],
276
+ version_file: File.join("config", "version.rb")
277
+ }
278
+ end
279
+
280
+ ##
281
+ # Sinatra apps should not infer a package name from config/version.rb.
282
+ #
283
+ # @return [Boolean]
284
+ def infer_package_name?
285
+ false
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ Semverve::Adapters.register(Semverve::Adapters::Rails.new)
292
+ Semverve::Adapters.register(Semverve::Adapters::Sinatra.new)
@@ -2,9 +2,10 @@
2
2
 
3
3
  require "rake"
4
4
 
5
+ require_relative "adapters"
5
6
  require_relative "error"
6
- require_relative "presets"
7
7
  require_relative "project_metadata"
8
+ require_relative "version_checks"
8
9
 
9
10
  module Semverve
10
11
  ##
@@ -14,7 +15,13 @@ module Semverve
14
15
  # Version-maintenance surfaces supported by umbrella check and fix tasks.
15
16
  #
16
17
  # @return [Array<Symbol>]
17
- VALID_VERSION_CHECKS = [:doc_references, :code_references, :metadata].freeze
18
+ VALID_VERSION_CHECKS = VersionChecks.names(extra_checks: Adapters.checks).freeze
19
+
20
+ ##
21
+ # Default version-maintenance surfaces for package projects.
22
+ #
23
+ # @return [Array<Symbol>]
24
+ DEFAULT_VERSION_CHECKS = VersionChecks.default_names.freeze
18
25
 
19
26
  ##
20
27
  # Release-readiness surfaces supported by release check tasks.
@@ -26,7 +33,13 @@ module Semverve
26
33
  # Default Ruby pattern for code version literals that are safe to rewrite.
27
34
  #
28
35
  # @return [Regexp]
29
- DEFAULT_VERSION_CODE_REFERENCE_PATTERN = /^\s*(?:(?:[A-Z]\w*::)*(?:[A-Z]\w*VERSION[A-Z0-9_]*|VERSION)|(?:[a-z_]\w*|self)\.version)\s*=\s*(?<quote>["'])(?<version>\d+\.\d+\.\d+)\k<quote>/
36
+ DEFAULT_VERSION_CODE_REFERENCE_PATTERN = VersionCodeReferences::RUBY_ASSIGNMENT_PATTERN
37
+
38
+ ##
39
+ # Framework adapter used to apply project defaults.
40
+ #
41
+ # @return [Symbol, String, nil]
42
+ attr_reader :adapter
30
43
 
31
44
  ##
32
45
  # Whether increments should run +bundle lock+ after writing a version.
@@ -62,7 +75,9 @@ module Semverve
62
75
  # Framework preset used to apply project defaults.
63
76
  #
64
77
  # @return [Symbol, String, nil]
65
- attr_reader :preset
78
+ def preset
79
+ adapter
80
+ end
66
81
 
67
82
  ##
68
83
  # Release-readiness surfaces run by the release check task.
@@ -113,10 +128,10 @@ module Semverve
113
128
  attr_reader :version_checks
114
129
 
115
130
  ##
116
- # Version-reference comparison mode.
131
+ # Version matching mode for reference and code-literal checks.
117
132
  #
118
133
  # @return [Symbol, String]
119
- attr_accessor :version_reference_mode
134
+ attr_accessor :version_match_mode
120
135
 
121
136
  ##
122
137
  # Initializes configuration with Semverve's default settings.
@@ -127,7 +142,7 @@ module Semverve
127
142
  @bundle_lock = false
128
143
  @command_runner = ->(command) { system(command) }
129
144
  @format = :module
130
- @preset = nil
145
+ @adapter = nil
131
146
  @rubygems_host = "https://rubygems.org"
132
147
  @version_code_reference_files = Rake::FileList[]
133
148
  self.version_code_reference_pattern = DEFAULT_VERSION_CODE_REFERENCE_PATTERN
@@ -137,9 +152,9 @@ module Semverve
137
152
  "tmp/**/*",
138
153
  "vendor/**/*"
139
154
  )
140
- @version_reference_mode = :older
155
+ @version_match_mode = :older
141
156
  self.release_checks = []
142
- self.version_checks = VALID_VERSION_CHECKS
157
+ @version_checks = DEFAULT_VERSION_CHECKS
143
158
  end
144
159
 
145
160
  ##
@@ -163,7 +178,7 @@ module Semverve
163
178
  version_code_reference_files: version_code_reference_files,
164
179
  version_code_reference_pattern: version_code_reference_pattern,
165
180
  version_doc_reference_files: version_doc_reference_files,
166
- version_reference_mode: normalized_version_reference_mode
181
+ version_match_mode: normalized_version_match_mode
167
182
  )
168
183
  end
169
184
 
@@ -178,7 +193,7 @@ module Semverve
178
193
  end
179
194
 
180
195
  ##
181
- # Sets the gem name used for metadata checks.
196
+ # Sets the gem name used for package metadata checks.
182
197
  #
183
198
  # @param [String, nil] gem_name
184
199
  #
@@ -197,6 +212,17 @@ module Semverve
197
212
  set_explicit(:module_name, module_name)
198
213
  end
199
214
 
215
+ ##
216
+ # Sets the framework adapter used for project defaults.
217
+ #
218
+ # @param [Symbol, String, nil] adapter
219
+ #
220
+ # @return [Symbol, nil]
221
+ def adapter=(adapter)
222
+ @adapter_defaults = nil
223
+ @adapter = adapter.nil? ? nil : Adapters.fetch(adapter).name
224
+ end
225
+
200
226
  ##
201
227
  # Sets the framework preset used for project defaults.
202
228
  #
@@ -204,8 +230,7 @@ module Semverve
204
230
  #
205
231
  # @return [Symbol, nil]
206
232
  def preset=(preset)
207
- @preset_defaults = nil
208
- @preset = preset.nil? ? nil : Presets.fetch(preset).name
233
+ self.adapter = preset
209
234
  end
210
235
 
211
236
  ##
@@ -256,7 +281,7 @@ module Semverve
256
281
  #
257
282
  # @return [Array<Symbol>]
258
283
  def version_checks=(checks)
259
- @version_checks = normalize_version_checks(checks)
284
+ set_explicit(:version_checks, normalize_version_checks(checks))
260
285
  end
261
286
 
262
287
  ##
@@ -291,25 +316,33 @@ module Semverve
291
316
  # @return [Object]
292
317
  def resolved_value(attribute)
293
318
  return public_send(attribute) if explicit_attribute?(attribute)
294
- return preset_defaults[attribute] if preset_defaults.key?(attribute)
319
+ return adapter_defaults[attribute] if adapter_defaults.key?(attribute)
295
320
 
296
321
  public_send(attribute)
297
322
  end
298
323
 
324
+ ##
325
+ # Whether package names should be inferred from project files.
326
+ #
327
+ # @return [Boolean]
328
+ def infer_package_name?
329
+ Adapters.infer_package_name?(adapter)
330
+ end
331
+
299
332
  ##
300
333
  # Configured version checks normalized for lookup.
301
334
  #
302
335
  # @return [Array<Symbol>]
303
336
  def normalized_version_checks
304
- normalize_version_checks(version_checks)
337
+ normalize_version_checks(resolved_value(:version_checks))
305
338
  end
306
339
 
307
340
  ##
308
- # Configured version-reference mode normalized for lookup.
341
+ # Configured version matching mode normalized for lookup.
309
342
  #
310
343
  # @return [Symbol]
311
- def normalized_version_reference_mode
312
- version_reference_mode.to_sym
344
+ def normalized_version_match_mode
345
+ version_match_mode.to_sym
313
346
  end
314
347
 
315
348
  ##
@@ -319,15 +352,7 @@ module Semverve
319
352
  #
320
353
  # @return [Array<Symbol>]
321
354
  def normalize_version_checks(checks)
322
- normalized_checks = Array(checks).map do |check|
323
- check.respond_to?(:to_sym) ? check.to_sym : check
324
- end
325
- return normalized_checks if normalized_checks.all? { |check| VALID_VERSION_CHECKS.include?(check) }
326
-
327
- invalid_checks = normalized_checks.reject { |check| VALID_VERSION_CHECKS.include?(check) }
328
- valid_check_names = VALID_VERSION_CHECKS.map(&:inspect)
329
- valid_checks = "#{valid_check_names[0...-1].join(", ")}, or #{valid_check_names.last}"
330
- raise Error, "Unknown version check #{invalid_checks.map(&:inspect).join(", ")}. Use #{valid_checks}."
355
+ VersionChecks.normalize(checks, extra_checks: Adapters.checks)
331
356
  end
332
357
 
333
358
  ##
@@ -382,17 +407,17 @@ module Semverve
382
407
  #
383
408
  # @return [Object]
384
409
  def set_explicit(attribute, value)
385
- @preset_defaults = nil
410
+ @adapter_defaults = nil
386
411
  @explicit_attributes << attribute unless explicit_attribute?(attribute)
387
412
  instance_variable_set(:"@#{attribute}", value)
388
413
  end
389
414
 
390
415
  ##
391
- # Defaults supplied by the configured preset.
416
+ # Defaults supplied by the configured adapter.
392
417
  #
393
418
  # @return [Hash]
394
- def preset_defaults
395
- @preset_defaults ||= Presets.defaults_for(preset, self)
419
+ def adapter_defaults
420
+ @adapter_defaults ||= Adapters.defaults_for(adapter, self)
396
421
  end
397
422
  end
398
423
 
@@ -420,7 +445,7 @@ module Semverve
420
445
  ##
421
446
  # Resolved gem name.
422
447
  #
423
- # @return [String]
448
+ # @return [String, nil]
424
449
  attr_reader :gem_name
425
450
 
426
451
  ##
@@ -478,10 +503,10 @@ module Semverve
478
503
  attr_reader :version_doc_reference_files
479
504
 
480
505
  ##
481
- # Resolved version-reference comparison mode.
506
+ # Resolved version matching mode for reference and code-literal checks.
482
507
  #
483
508
  # @return [Symbol]
484
- attr_reader :version_reference_mode
509
+ attr_reader :version_match_mode
485
510
 
486
511
  ##
487
512
  # Initializes a resolved configuration.
@@ -489,7 +514,7 @@ module Semverve
489
514
  # @param [Boolean] bundle_lock
490
515
  # @param [#call] command_runner
491
516
  # @param [Symbol] format
492
- # @param [String] gem_name
517
+ # @param [String, nil] gem_name
493
518
  # @param [String] module_name
494
519
  # @param [Array<Symbol>] release_checks
495
520
  # @param [String] root
@@ -499,7 +524,7 @@ module Semverve
499
524
  # @param [Rake::FileList] version_code_reference_files
500
525
  # @param [Regexp] version_code_reference_pattern
501
526
  # @param [Rake::FileList] version_doc_reference_files
502
- # @param [Symbol] version_reference_mode
527
+ # @param [Symbol] version_match_mode
503
528
  #
504
529
  # @return [Semverve::ResolvedConfiguration]
505
530
  def initialize(
@@ -516,7 +541,7 @@ module Semverve
516
541
  version_code_reference_files:,
517
542
  version_code_reference_pattern:,
518
543
  version_doc_reference_files:,
519
- version_reference_mode:
544
+ version_match_mode:
520
545
  )
521
546
  @bundle_lock = bundle_lock
522
547
  @command_runner = command_runner
@@ -531,7 +556,7 @@ module Semverve
531
556
  @version_code_reference_files = version_code_reference_files
532
557
  @version_code_reference_pattern = version_code_reference_pattern
533
558
  @version_doc_reference_files = version_doc_reference_files
534
- @version_reference_mode = version_reference_mode
559
+ @version_match_mode = version_match_mode
535
560
  end
536
561
 
537
562
  ##
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Semverve
4
+ ##
5
+ # A version mismatch found by a Semverve check.
6
+ class Finding
7
+ ##
8
+ # Path relative to the configured project root.
9
+ #
10
+ # @return [String]
11
+ attr_reader :path
12
+
13
+ ##
14
+ # One-based line number.
15
+ #
16
+ # @return [Integer]
17
+ attr_reader :line
18
+
19
+ ##
20
+ # One-based column number.
21
+ #
22
+ # @return [Integer]
23
+ attr_reader :column
24
+
25
+ ##
26
+ # Referenced semantic version.
27
+ #
28
+ # @return [Semverve::SemanticVersion]
29
+ attr_reader :version
30
+
31
+ ##
32
+ # Optional output label for findings that carry their own label.
33
+ #
34
+ # @return [String, nil]
35
+ attr_reader :label
36
+
37
+ ##
38
+ # Initializes a finding.
39
+ #
40
+ # @param [String] path
41
+ # @param [Integer] line
42
+ # @param [Integer] column
43
+ # @param [Semverve::SemanticVersion] version
44
+ # @param [String, nil] label
45
+ #
46
+ # @return [Semverve::Finding]
47
+ def initialize(path:, line:, column:, version:, label: nil)
48
+ @path = path
49
+ @line = line
50
+ @column = column
51
+ @version = version
52
+ @label = label
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Semverve
4
+ ##
5
+ # Result of fixing one Semverve check surface.
6
+ class FixResult
7
+ ##
8
+ # Files changed by the fix.
9
+ #
10
+ # @return [Array<String>]
11
+ attr_reader :changed_files
12
+
13
+ ##
14
+ # Number of replacements made.
15
+ #
16
+ # @return [Integer]
17
+ attr_reader :replacement_count
18
+
19
+ ##
20
+ # Whether the fix ran +bundle lock+.
21
+ #
22
+ # @return [Boolean]
23
+ attr_reader :bundle_lock_ran
24
+
25
+ ##
26
+ # Initializes a fix result.
27
+ #
28
+ # @param [Array<String>] changed_files
29
+ # @param [Integer] replacement_count
30
+ # @param [Boolean] bundle_lock_ran
31
+ #
32
+ # @return [Semverve::FixResult]
33
+ def initialize(changed_files:, replacement_count:, bundle_lock_ran: false)
34
+ @changed_files = changed_files
35
+ @replacement_count = replacement_count
36
+ @bundle_lock_ran = bundle_lock_ran
37
+ end
38
+ end
39
+ end