semverve 0.3.0 → 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,518 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+ require_relative "package_metadata"
5
+ require_relative "rails_config_metadata"
6
+ require_relative "version_code_references"
7
+ require_relative "version_references"
8
+
9
+ module Semverve
10
+ ##
11
+ # Registry and check adapters for version-maintenance surfaces.
12
+ module VersionChecks
13
+ class << self
14
+ ##
15
+ # Registers a core version check.
16
+ #
17
+ # @param [#name] check
18
+ #
19
+ # @return [#name]
20
+ def register(check)
21
+ core_checks[check.name] = check
22
+ end
23
+
24
+ ##
25
+ # All checks available to task dispatch and validation.
26
+ #
27
+ # @param [Array<#name>] extra_checks
28
+ #
29
+ # @return [Array<#name>]
30
+ def all(extra_checks: [])
31
+ (core_checks.values + extra_checks).each_with_object({}) do |check, checks|
32
+ checks[check.name] = check
33
+ end.values
34
+ end
35
+
36
+ ##
37
+ # Fetches a registered check by name.
38
+ #
39
+ # @param [Symbol, String] name
40
+ # @param [Array<#name>] extra_checks
41
+ #
42
+ # @return [#name]
43
+ def fetch(name, extra_checks: [])
44
+ normalized_name = normalize_name(name)
45
+ check = all(extra_checks: extra_checks).find { |candidate| candidate.name == normalized_name }
46
+ return check if check
47
+
48
+ raise Error, unknown_check_message([normalized_name], extra_checks: extra_checks)
49
+ end
50
+
51
+ ##
52
+ # Validates and normalizes configured check names.
53
+ #
54
+ # @param [Array<Symbol, String>] checks
55
+ # @param [Array<#name>] extra_checks
56
+ #
57
+ # @return [Array<Symbol>]
58
+ def normalize(checks, extra_checks: [])
59
+ normalized_checks = Array(checks).map { |check| normalize_name(check) }
60
+ valid_names = names(extra_checks: extra_checks)
61
+ return normalized_checks if normalized_checks.all? { |check| valid_names.include?(check) }
62
+
63
+ invalid_checks = normalized_checks.reject { |check| valid_names.include?(check) }
64
+ raise Error, unknown_check_message(invalid_checks, extra_checks: extra_checks)
65
+ end
66
+
67
+ ##
68
+ # Registered check names.
69
+ #
70
+ # @param [Array<#name>] extra_checks
71
+ #
72
+ # @return [Array<Symbol>]
73
+ def names(extra_checks: [])
74
+ all(extra_checks: extra_checks).map(&:name)
75
+ end
76
+
77
+ ##
78
+ # Default core version-maintenance surfaces for package projects.
79
+ #
80
+ # @return [Array<Symbol>]
81
+ def default_names
82
+ [:doc_references, :code_references, :package_metadata]
83
+ end
84
+
85
+ private
86
+
87
+ ##
88
+ # Core check registry.
89
+ #
90
+ # @return [Hash<Symbol, #name>]
91
+ def core_checks
92
+ @core_checks ||= {}
93
+ end
94
+
95
+ ##
96
+ # Normalizes a check name.
97
+ #
98
+ # @param [Object] name
99
+ #
100
+ # @return [Symbol, Object]
101
+ def normalize_name(name)
102
+ name.respond_to?(:to_sym) ? name.to_sym : name
103
+ end
104
+
105
+ ##
106
+ # Error message for invalid checks.
107
+ #
108
+ # @param [Array<Symbol>] invalid_checks
109
+ # @param [Array<#name>] extra_checks
110
+ #
111
+ # @return [String]
112
+ def unknown_check_message(invalid_checks, extra_checks:)
113
+ valid_check_names = names(extra_checks: extra_checks).map(&:inspect)
114
+ valid_checks = "#{valid_check_names[0...-1].join(", ")}, or #{valid_check_names.last}"
115
+ "Unknown version check #{invalid_checks.map(&:inspect).join(", ")}. Use #{valid_checks}."
116
+ end
117
+ end
118
+
119
+ ##
120
+ # Base behavior for check adapters. Public check objects must expose name,
121
+ # task_name, descriptions, labels, findings, and fix.
122
+ class Check
123
+ ##
124
+ # Whether focused task accepts an exact target version argument.
125
+ #
126
+ # @return [Boolean]
127
+ def targetable?
128
+ false
129
+ end
130
+
131
+ ##
132
+ # Whether findings should print a note when an exact target is already current.
133
+ #
134
+ # @return [Boolean]
135
+ def exact_target_fix_noop_notice?
136
+ false
137
+ end
138
+
139
+ ##
140
+ # Rake task argument names for the focused task.
141
+ #
142
+ # @return [Array<Symbol>]
143
+ def task_arguments
144
+ targetable? ? [:version] : []
145
+ end
146
+
147
+ ##
148
+ # Label used when reporting replacement counts.
149
+ #
150
+ # @return [String]
151
+ def fix_label
152
+ finding_label
153
+ end
154
+
155
+ ##
156
+ # Finds mismatches.
157
+ #
158
+ # @return [Array]
159
+ def findings(_configuration, _current_version, include_ignored: false, target_version: nil)
160
+ raise NotImplementedError
161
+ end
162
+
163
+ ##
164
+ # Fixes mismatches.
165
+ #
166
+ # @return [#changed_files, #replacement_count]
167
+ def fix(_configuration, _current_version, target_version: nil)
168
+ raise NotImplementedError
169
+ end
170
+ end
171
+
172
+ ##
173
+ # Documentation/reference version check.
174
+ class DocReferences < Check
175
+ ##
176
+ # Initializes the check.
177
+ #
178
+ # @param [Class] scanner
179
+ #
180
+ # @return [Semverve::VersionChecks::DocReferences]
181
+ def initialize(scanner: VersionReferences)
182
+ @scanner = scanner
183
+ end
184
+
185
+ ##
186
+ # Public check name used in +config.version_checks+.
187
+ #
188
+ # @return [Symbol]
189
+ def name
190
+ :doc_references
191
+ end
192
+
193
+ ##
194
+ # Focused Rake task suffix.
195
+ #
196
+ # @return [Symbol]
197
+ def task_name
198
+ :references
199
+ end
200
+
201
+ ##
202
+ # Focused check task description.
203
+ #
204
+ # @return [String]
205
+ def check_description
206
+ "Check configured files for stale version references"
207
+ end
208
+
209
+ ##
210
+ # Focused fix task description.
211
+ #
212
+ # @return [String]
213
+ def fix_description
214
+ "Replace stale version references in configured files"
215
+ end
216
+
217
+ ##
218
+ # Label printed for findings and replacement counts.
219
+ #
220
+ # @return [String]
221
+ def finding_label
222
+ "version reference"
223
+ end
224
+
225
+ ##
226
+ # Message printed when no findings or replacements exist.
227
+ #
228
+ # @return [String]
229
+ def clean_message
230
+ "Version references are current."
231
+ end
232
+
233
+ def targetable?
234
+ true
235
+ end
236
+
237
+ def exact_target_fix_noop_notice?
238
+ true
239
+ end
240
+
241
+ def findings(configuration, current_version, include_ignored: false, target_version: nil)
242
+ scanner.new(
243
+ configuration,
244
+ current_version,
245
+ include_ignored: include_ignored,
246
+ target_version: target_version
247
+ ).findings
248
+ end
249
+
250
+ def fix(configuration, current_version, target_version: nil)
251
+ scanner.new(configuration, current_version, target_version: target_version).fix
252
+ end
253
+
254
+ private
255
+
256
+ ##
257
+ # Scanner/fixer class used by the check.
258
+ #
259
+ # @return [Class]
260
+ attr_reader :scanner
261
+ end
262
+
263
+ ##
264
+ # Code literal version check.
265
+ class CodeReferences < Check
266
+ ##
267
+ # Initializes the check.
268
+ #
269
+ # @param [Class] scanner
270
+ #
271
+ # @return [Semverve::VersionChecks::CodeReferences]
272
+ def initialize(scanner: VersionCodeReferences)
273
+ @scanner = scanner
274
+ end
275
+
276
+ ##
277
+ # Public check name used in +config.version_checks+.
278
+ #
279
+ # @return [Symbol]
280
+ def name
281
+ :code_references
282
+ end
283
+
284
+ ##
285
+ # Focused Rake task suffix.
286
+ #
287
+ # @return [Symbol]
288
+ def task_name
289
+ :code
290
+ end
291
+
292
+ ##
293
+ # Focused check task description.
294
+ #
295
+ # @return [String]
296
+ def check_description
297
+ "Check configured code files for version literals"
298
+ end
299
+
300
+ ##
301
+ # Focused fix task description.
302
+ #
303
+ # @return [String]
304
+ def fix_description
305
+ "Replace safe code version literals in configured files"
306
+ end
307
+
308
+ ##
309
+ # Label printed for findings and replacement counts.
310
+ #
311
+ # @return [String]
312
+ def finding_label
313
+ "code version literal"
314
+ end
315
+
316
+ ##
317
+ # Message printed when no findings or replacements exist.
318
+ #
319
+ # @return [String]
320
+ def clean_message
321
+ "Code version literals are current."
322
+ end
323
+
324
+ def targetable?
325
+ true
326
+ end
327
+
328
+ def exact_target_fix_noop_notice?
329
+ true
330
+ end
331
+
332
+ def findings(configuration, current_version, include_ignored: false, target_version: nil)
333
+ scanner.new(
334
+ configuration,
335
+ current_version,
336
+ include_ignored: include_ignored,
337
+ target_version: target_version
338
+ ).findings
339
+ end
340
+
341
+ def fix(configuration, current_version, target_version: nil)
342
+ scanner.new(configuration, current_version, target_version: target_version).fix
343
+ end
344
+
345
+ private
346
+
347
+ ##
348
+ # Scanner/fixer class used by the check.
349
+ #
350
+ # @return [Class]
351
+ attr_reader :scanner
352
+ end
353
+
354
+ ##
355
+ # Package metadata version check.
356
+ class PackageMetadataCheck < Check
357
+ ##
358
+ # Initializes the check.
359
+ #
360
+ # @param [Class] scanner
361
+ #
362
+ # @return [Semverve::VersionChecks::PackageMetadataCheck]
363
+ def initialize(scanner: PackageMetadata)
364
+ @scanner = scanner
365
+ end
366
+
367
+ ##
368
+ # Public check name used in +config.version_checks+.
369
+ #
370
+ # @return [Symbol]
371
+ def name
372
+ :package_metadata
373
+ end
374
+
375
+ ##
376
+ # Focused Rake task suffix.
377
+ #
378
+ # @return [Symbol]
379
+ def task_name
380
+ :package_metadata
381
+ end
382
+
383
+ ##
384
+ # Focused check task description.
385
+ #
386
+ # @return [String]
387
+ def check_description
388
+ "Check package metadata for version mismatches"
389
+ end
390
+
391
+ ##
392
+ # Focused fix task description.
393
+ #
394
+ # @return [String]
395
+ def fix_description
396
+ "Fix safe package metadata version mismatches"
397
+ end
398
+
399
+ ##
400
+ # Package findings provide their own labels.
401
+ #
402
+ # @return [nil]
403
+ def finding_label
404
+ nil
405
+ end
406
+
407
+ def fix_label
408
+ "package metadata version"
409
+ end
410
+
411
+ ##
412
+ # Message printed when no findings or replacements exist.
413
+ #
414
+ # @return [String]
415
+ def clean_message
416
+ "Package metadata is current."
417
+ end
418
+
419
+ def findings(configuration, current_version, include_ignored: false, target_version: nil)
420
+ scanner.new(configuration, current_version).findings
421
+ end
422
+
423
+ def fix(configuration, current_version, target_version: nil)
424
+ scanner.new(configuration, current_version).fix
425
+ end
426
+
427
+ private
428
+
429
+ ##
430
+ # Scanner/fixer class used by the check.
431
+ #
432
+ # @return [Class]
433
+ attr_reader :scanner
434
+ end
435
+
436
+ ##
437
+ # Rails config metadata version check.
438
+ class RailsConfigMetadataCheck < Check
439
+ ##
440
+ # Initializes the check.
441
+ #
442
+ # @param [Class] scanner
443
+ #
444
+ # @return [Semverve::VersionChecks::RailsConfigMetadataCheck]
445
+ def initialize(scanner: RailsConfigMetadata)
446
+ @scanner = scanner
447
+ end
448
+
449
+ ##
450
+ # Public check name used in +config.version_checks+.
451
+ #
452
+ # @return [Symbol]
453
+ def name
454
+ :rails_config_metadata
455
+ end
456
+
457
+ ##
458
+ # Focused Rake task suffix.
459
+ #
460
+ # @return [Symbol]
461
+ def task_name
462
+ :rails_config_metadata
463
+ end
464
+
465
+ ##
466
+ # Focused check task description.
467
+ #
468
+ # @return [String]
469
+ def check_description
470
+ "Check Rails config metadata for version mismatches"
471
+ end
472
+
473
+ ##
474
+ # Focused fix task description.
475
+ #
476
+ # @return [String]
477
+ def fix_description
478
+ "Fix safe Rails config metadata version mismatches"
479
+ end
480
+
481
+ ##
482
+ # Label printed for Rails config metadata findings and replacements.
483
+ #
484
+ # @return [String]
485
+ def finding_label
486
+ "Rails config version"
487
+ end
488
+
489
+ ##
490
+ # Message printed when no findings or replacements exist.
491
+ #
492
+ # @return [String]
493
+ def clean_message
494
+ "Rails config metadata is current."
495
+ end
496
+
497
+ def findings(configuration, current_version, include_ignored: false, target_version: nil)
498
+ scanner.new(configuration, current_version).findings
499
+ end
500
+
501
+ def fix(configuration, current_version, target_version: nil)
502
+ scanner.new(configuration, current_version).fix
503
+ end
504
+
505
+ private
506
+
507
+ ##
508
+ # Scanner/fixer class used by the check.
509
+ #
510
+ # @return [Class]
511
+ attr_reader :scanner
512
+ end
513
+ end
514
+ end
515
+
516
+ Semverve::VersionChecks.register(Semverve::VersionChecks::DocReferences.new)
517
+ Semverve::VersionChecks.register(Semverve::VersionChecks::CodeReferences.new)
518
+ Semverve::VersionChecks.register(Semverve::VersionChecks::PackageMetadataCheck.new)
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "configuration"
4
3
  require_relative "file_list_resolver"
4
+ require_relative "finding"
5
+ require_relative "fix_result"
5
6
  require_relative "semantic_version"
7
+ require_relative "version_literal_rewriter"
8
+ require_relative "version_match_policy"
6
9
 
7
10
  module Semverve
8
11
  ##
@@ -18,79 +21,7 @@ module Semverve
18
21
  # Ruby assignments that are safe enough to rewrite automatically.
19
22
  #
20
23
  # @return [Regexp]
21
- RUBY_ASSIGNMENT_PATTERN = Configuration::DEFAULT_VERSION_CODE_REFERENCE_PATTERN
22
-
23
- ##
24
- # A code version literal found in a configured file.
25
- class Finding
26
- ##
27
- # Path relative to the configured project root.
28
- #
29
- # @return [String]
30
- attr_reader :path
31
-
32
- ##
33
- # One-based line number.
34
- #
35
- # @return [Integer]
36
- attr_reader :line
37
-
38
- ##
39
- # One-based column number.
40
- #
41
- # @return [Integer]
42
- attr_reader :column
43
-
44
- ##
45
- # Referenced semantic version.
46
- #
47
- # @return [Semverve::SemanticVersion]
48
- attr_reader :version
49
-
50
- ##
51
- # Initializes a finding.
52
- #
53
- # @param [String] path
54
- # @param [Integer] line
55
- # @param [Integer] column
56
- # @param [Semverve::SemanticVersion] version
57
- #
58
- # @return [Semverve::VersionCodeReferences::Finding]
59
- def initialize(path:, line:, column:, version:)
60
- @path = path
61
- @line = line
62
- @column = column
63
- @version = version
64
- end
65
- end
66
-
67
- ##
68
- # Result of fixing code version literals.
69
- class FixResult
70
- ##
71
- # Files changed by the fix.
72
- #
73
- # @return [Array<String>]
74
- attr_reader :changed_files
75
-
76
- ##
77
- # Number of replacements made.
78
- #
79
- # @return [Integer]
80
- attr_reader :replacement_count
81
-
82
- ##
83
- # Initializes a fix result.
84
- #
85
- # @param [Array<String>] changed_files
86
- # @param [Integer] replacement_count
87
- #
88
- # @return [Semverve::VersionCodeReferences::FixResult]
89
- def initialize(changed_files:, replacement_count:)
90
- @changed_files = changed_files
91
- @replacement_count = replacement_count
92
- end
93
- end
24
+ RUBY_ASSIGNMENT_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>/
94
25
 
95
26
  ##
96
27
  # Initializes code version literal scanning.
@@ -109,7 +40,7 @@ module Semverve
109
40
  ##
110
41
  # Code version literal findings in configured files.
111
42
  #
112
- # @return [Array<Semverve::VersionCodeReferences::Finding>]
43
+ # @return [Array<Semverve::Finding>]
113
44
  def findings
114
45
  files.flat_map { |path| findings_for_file(path) }
115
46
  end
@@ -117,7 +48,7 @@ module Semverve
117
48
  ##
118
49
  # Replaces found code version literals with the current version.
119
50
  #
120
- # @return [Semverve::VersionCodeReferences::FixResult]
51
+ # @return [Semverve::FixResult]
121
52
  def fix
122
53
  changed_files = []
123
54
  replacement_count = 0
@@ -133,7 +64,7 @@ module Semverve
133
64
  replacement_count += count
134
65
  end
135
66
 
136
- FixResult.new(changed_files: changed_files, replacement_count: replacement_count)
67
+ Semverve::FixResult.new(changed_files: changed_files, replacement_count: replacement_count)
137
68
  end
138
69
 
139
70
  private
@@ -194,7 +125,7 @@ module Semverve
194
125
  #
195
126
  # @param [String] path
196
127
  #
197
- # @return [Array<Semverve::VersionCodeReferences::Finding>]
128
+ # @return [Array<Semverve::Finding>]
198
129
  def findings_for_file(path)
199
130
  lines = File.readlines(path)
200
131
 
@@ -207,7 +138,7 @@ module Semverve
207
138
  version = SemanticVersion.parse(match[:version])
208
139
  next unless report?(version)
209
140
 
210
- Finding.new(
141
+ Semverve::Finding.new(
211
142
  path: relative_path(path),
212
143
  line: line_number,
213
144
  column: match.begin(:version) + 1,
@@ -268,14 +199,7 @@ module Semverve
268
199
  def report?(version)
269
200
  return version == target_version if target_version
270
201
 
271
- case configuration.version_match_mode
272
- when :older
273
- version < current_version
274
- when :non_current
275
- version != current_version
276
- else
277
- raise Error, "Unknown version match mode #{configuration.version_match_mode.inspect}. Use :older or :non_current."
278
- end
202
+ match_policy.report?(version)
279
203
  end
280
204
 
281
205
  ##
@@ -285,13 +209,7 @@ module Semverve
285
209
  #
286
210
  # @return [String]
287
211
  def replace_matched_version(line)
288
- line.sub(pattern) do |matched_text|
289
- match = Regexp.last_match
290
- version_start = match.begin(:version) - match.begin(0)
291
- version_end = match.end(:version) - match.begin(0)
292
-
293
- "#{matched_text[0...version_start]}#{current_version}#{matched_text[version_end..]}"
294
- end
212
+ literal_rewriter.rewrite(line)
295
213
  end
296
214
 
297
215
  ##
@@ -303,5 +221,25 @@ module Semverve
303
221
  def relative_path(path)
304
222
  path.delete_prefix("#{root}/")
305
223
  end
224
+
225
+ ##
226
+ # Version matching behavior for code literals.
227
+ #
228
+ # @return [Semverve::VersionMatchPolicy]
229
+ def match_policy
230
+ @match_policy ||= VersionMatchPolicy.new(
231
+ current_version: current_version,
232
+ match_mode: configuration.version_match_mode,
233
+ target_version: target_version
234
+ )
235
+ end
236
+
237
+ ##
238
+ # Safe literal rewriter for the configured pattern.
239
+ #
240
+ # @return [Semverve::VersionLiteralRewriter]
241
+ def literal_rewriter
242
+ @literal_rewriter ||= VersionLiteralRewriter.new(pattern: pattern, replacement: current_version)
243
+ end
306
244
  end
307
245
  end