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.
@@ -4,7 +4,7 @@ require_relative "error"
4
4
 
5
5
  module Semverve
6
6
  ##
7
- # Infers gem metadata from explicit configuration or project files.
7
+ # Infers project metadata from explicit configuration or project files.
8
8
  class ProjectMetadata
9
9
  ##
10
10
  # Initializes project metadata inference.
@@ -18,9 +18,12 @@ module Semverve
18
18
 
19
19
  ##
20
20
  # Gem name from explicit config, the version-file path, or the gemspec.
21
+ # App adapters can disable package-name inference.
21
22
  #
22
- # @return [String]
23
+ # @return [String, nil]
23
24
  def gem_name
25
+ return explicit_gem_name || configured_gem_name unless configuration.infer_package_name?
26
+
24
27
  explicit_gem_name || configured_version_file_gem_name || gemspec_name
25
28
  end
26
29
 
@@ -57,7 +60,7 @@ module Semverve
57
60
  end
58
61
 
59
62
  ##
60
- # Configured gem name after applying presets.
63
+ # Configured gem name after applying adapters.
61
64
  #
62
65
  # @return [String, nil]
63
66
  def configured_gem_name
@@ -73,7 +76,7 @@ module Semverve
73
76
  end
74
77
 
75
78
  ##
76
- # Configured Ruby module name after applying presets.
79
+ # Configured Ruby module name after applying adapters.
77
80
  #
78
81
  # @return [String, nil]
79
82
  def configured_module_name
@@ -81,7 +84,7 @@ module Semverve
81
84
  end
82
85
 
83
86
  ##
84
- # Configured version-file path after applying presets.
87
+ # Configured version-file path after applying adapters.
85
88
  #
86
89
  # @return [String, nil]
87
90
  def configured_version_file
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+
5
+ require_relative "file_list_resolver"
6
+ require_relative "finding"
7
+ require_relative "fix_result"
8
+ require_relative "semantic_version"
9
+ require_relative "version_literal_rewriter"
10
+ require_relative "version_match_policy"
11
+
12
+ module Semverve
13
+ ##
14
+ # Finds and updates Rails-native config version literals.
15
+ class RailsConfigMetadata
16
+ ##
17
+ # Default Rails config file patterns that can expose an app version.
18
+ #
19
+ # @return [Array<String>]
20
+ DEFAULT_FILE_PATTERNS = [
21
+ "config/application.rb",
22
+ "config/environments/*.rb",
23
+ "config/initializers/**/*.rb"
24
+ ].freeze
25
+
26
+ ##
27
+ # Rails config version assignments that are safe to rewrite.
28
+ #
29
+ # @return [Regexp]
30
+ VERSION_PATTERN = /^\s*(?:Rails\.application\.)?config\.x\.version\s*=\s*(?<quote>["'])(?<version>\d+\.\d+\.\d+)\k<quote>/
31
+
32
+ ##
33
+ # Initializes Rails config metadata scanning.
34
+ #
35
+ # @param [Semverve::ResolvedConfiguration] configuration
36
+ # @param [Semverve::SemanticVersion] current_version
37
+ #
38
+ # @return [Semverve::RailsConfigMetadata]
39
+ def initialize(configuration, current_version)
40
+ @configuration = configuration
41
+ @current_version = current_version
42
+ end
43
+
44
+ ##
45
+ # Rails config metadata findings.
46
+ #
47
+ # @return [Array<Semverve::Finding>]
48
+ def findings
49
+ files.flat_map { |path| findings_for_file(path) }
50
+ end
51
+
52
+ ##
53
+ # Replaces safe stale Rails config version literals with the current version.
54
+ #
55
+ # @return [Semverve::FixResult]
56
+ def fix
57
+ changed_files = []
58
+ replacement_count = 0
59
+
60
+ files.each do |path|
61
+ content = File.read(path)
62
+ fixed, count = fixed_content(content)
63
+
64
+ next if count.zero?
65
+
66
+ File.write(path, fixed)
67
+ changed_files << relative_path(path)
68
+ replacement_count += count
69
+ end
70
+
71
+ Semverve::FixResult.new(changed_files: changed_files, replacement_count: replacement_count)
72
+ end
73
+
74
+ private
75
+
76
+ ##
77
+ # Resolved Semverve configuration.
78
+ #
79
+ # @return [Semverve::ResolvedConfiguration]
80
+ attr_reader :configuration
81
+
82
+ ##
83
+ # Current app version.
84
+ #
85
+ # @return [Semverve::SemanticVersion]
86
+ attr_reader :current_version
87
+
88
+ ##
89
+ # Absolute configured project root.
90
+ #
91
+ # @return [String]
92
+ def root
93
+ configuration.root
94
+ end
95
+
96
+ ##
97
+ # Absolute files to scan.
98
+ #
99
+ # @return [Array<String>]
100
+ def files
101
+ @files ||= FileListResolver.new(root: root, file_list: Rake::FileList[*DEFAULT_FILE_PATTERNS]).files
102
+ end
103
+
104
+ ##
105
+ # Findings for a single file.
106
+ #
107
+ # @param [String] path
108
+ #
109
+ # @return [Array<Semverve::Finding>]
110
+ def findings_for_file(path)
111
+ File.readlines(path).filter_map.with_index(1) do |line, line_number|
112
+ match = line.match(VERSION_PATTERN)
113
+ next unless match
114
+
115
+ version = SemanticVersion.parse(match[:version])
116
+ next unless match_policy.report?(version)
117
+
118
+ Semverve::Finding.new(
119
+ path: relative_path(path),
120
+ line: line_number,
121
+ column: match.begin(:version) + 1,
122
+ version: version
123
+ )
124
+ end
125
+ end
126
+
127
+ ##
128
+ # Fixed content and replacement count.
129
+ #
130
+ # @param [String] content
131
+ #
132
+ # @return [Array(String, Integer)]
133
+ def fixed_content(content)
134
+ replacement_count = 0
135
+
136
+ fixed = content.lines.map do |line|
137
+ match = line.match(VERSION_PATTERN)
138
+ next line unless match
139
+
140
+ version = SemanticVersion.parse(match[:version])
141
+ next line unless match_policy.report?(version)
142
+
143
+ replacement_count += 1
144
+ literal_rewriter.rewrite(line)
145
+ end.join
146
+
147
+ [fixed, replacement_count]
148
+ end
149
+
150
+ ##
151
+ # Path relative to the project root.
152
+ #
153
+ # @param [String] path
154
+ #
155
+ # @return [String]
156
+ def relative_path(path)
157
+ path.delete_prefix("#{root}/")
158
+ end
159
+
160
+ ##
161
+ # Rails config metadata always requires an exact current-version match.
162
+ #
163
+ # @return [Semverve::VersionMatchPolicy]
164
+ def match_policy
165
+ @match_policy ||= VersionMatchPolicy.new(current_version: current_version, match_mode: :non_current)
166
+ end
167
+
168
+ ##
169
+ # Safe literal rewriter for Rails config metadata.
170
+ #
171
+ # @return [Semverve::VersionLiteralRewriter]
172
+ def literal_rewriter
173
+ @literal_rewriter ||= VersionLiteralRewriter.new(pattern: VERSION_PATTERN, replacement: current_version)
174
+ end
175
+ end
176
+ end
@@ -7,7 +7,7 @@ module Semverve
7
7
  # Rails integration for installing Semverve's Rake tasks.
8
8
  class Railtie < ::Rails::Railtie
9
9
  rake_tasks do
10
- Semverve.configuration.preset = :rails unless Semverve.configuration.preset
10
+ Semverve.configuration.adapter = :rails unless Semverve.configuration.adapter
11
11
 
12
12
  require_relative "task"
13
13
  Semverve::Task.install
data/lib/semverve/task.rb CHANGED
@@ -3,13 +3,12 @@
3
3
  require "rake"
4
4
 
5
5
  require_relative "../semverve"
6
+ require_relative "adapters"
6
7
  require_relative "generator"
7
8
  require_relative "published_version"
8
9
  require_relative "semantic_version"
9
10
  require_relative "version_file"
10
- require_relative "version_code_references"
11
- require_relative "version_metadata"
12
- require_relative "version_references"
11
+ require_relative "version_checks"
13
12
 
14
13
  module Semverve
15
14
  ##
@@ -120,19 +119,11 @@ module Semverve
120
119
  end
121
120
 
122
121
  namespace :check do
123
- desc "Check configured files for stale version references"
124
- task :references, [:version] do |_task, args|
125
- check_references(args)
126
- end
127
-
128
- desc "Check configured code files for version literals"
129
- task :code, [:version] do |_task, args|
130
- check_code(args)
131
- end
132
-
133
- desc "Check gem metadata for version mismatches"
134
- task :metadata do
135
- check_metadata
122
+ version_checks.each do |version_check|
123
+ desc version_check.check_description
124
+ task version_check.task_name, version_check.task_arguments do |_task, args|
125
+ check_version_check(version_check, args)
126
+ end
136
127
  end
137
128
 
138
129
  desc "Check whether the current gem version is already published"
@@ -152,19 +143,11 @@ module Semverve
152
143
  end
153
144
 
154
145
  namespace :fix do
155
- desc "Replace stale version references in configured files"
156
- task :references, [:version] do |_task, args|
157
- fix_references(args)
158
- end
159
-
160
- desc "Replace safe code version literals in configured files"
161
- task :code, [:version] do |_task, args|
162
- fix_code(args)
163
- end
164
-
165
- desc "Fix safe gem metadata version mismatches"
166
- task :metadata do
167
- fix_metadata
146
+ version_checks.each do |version_check|
147
+ desc version_check.fix_description
148
+ task version_check.task_name, version_check.task_arguments do |_task, args|
149
+ fix_version_check(version_check, args)
150
+ end
168
151
  end
169
152
  end
170
153
  end
@@ -228,100 +211,70 @@ module Semverve
228
211
  end
229
212
 
230
213
  ##
231
- # Checks configured files for stale version references.
214
+ # Checks a single registered version check.
215
+ #
216
+ # @param [#findings] version_check
217
+ # @param [Rake::TaskArguments, nil] args
232
218
  #
233
219
  # @return [void]
234
- def check_references(args = nil)
220
+ def check_version_check(version_check, args = nil)
235
221
  configuration, current_version = check_context
236
- target_version = target_version_argument(args, "semverve:check:references")
222
+ target_version = check_target_version(version_check, args, "semverve:check:#{version_check.task_name}")
237
223
  report_findings(
238
224
  [[
239
- "version reference",
240
- VersionReferences.new(
225
+ version_check,
226
+ version_check.finding_label,
227
+ version_check.findings(
241
228
  configuration,
242
229
  current_version,
243
230
  include_ignored: report_ignored?,
244
231
  target_version: target_version
245
- ).findings
232
+ )
246
233
  ]],
247
234
  current_version,
248
235
  target_version: target_version,
249
- fix_task_name: "semverve:fix:references",
250
- clean_message: "Version references are current."
236
+ fix_task_name: "semverve:fix:#{version_check.task_name}",
237
+ clean_message: version_check.clean_message
251
238
  )
252
239
  end
253
240
 
254
241
  ##
255
- # Replaces stale version references in configured files.
242
+ # Fixes a single registered version check.
243
+ #
244
+ # @param [#fix] version_check
245
+ # @param [Rake::TaskArguments, nil] args
256
246
  #
257
247
  # @return [void]
258
- def fix_references(args = nil)
248
+ def fix_version_check(version_check, args = nil)
259
249
  configuration, current_version = check_context
260
- target_version = target_version_argument(args, "semverve:fix:references")
250
+ target_version = check_target_version(version_check, args, "semverve:fix:#{version_check.task_name}")
261
251
  return report_current_target_noop(target_version) if target_version == current_version
262
252
 
263
253
  report_fix_results(
264
254
  [[
265
- "version reference",
266
- VersionReferences.new(configuration, current_version, target_version: target_version).fix
255
+ version_check.fix_label,
256
+ version_check.fix(configuration, current_version, target_version: target_version)
267
257
  ]],
268
- clean_message: "Version references are current."
258
+ clean_message: version_check.clean_message
269
259
  )
270
260
  end
271
261
 
272
262
  ##
273
- # Checks configured code files for version literals.
263
+ # Registered version checks available to tasks.
274
264
  #
275
- # @return [void]
276
- def check_code(args = nil)
277
- configuration, current_version = check_context
278
- target_version = target_version_argument(args, "semverve:check:code")
279
- report_findings(
280
- [[
281
- "code version literal",
282
- VersionCodeReferences.new(
283
- configuration,
284
- current_version,
285
- include_ignored: report_ignored?,
286
- target_version: target_version
287
- ).findings
288
- ]],
289
- current_version,
290
- target_version: target_version,
291
- fix_task_name: "semverve:fix:code",
292
- clean_message: "Code version literals are current."
293
- )
265
+ # @return [Array<#name>]
266
+ def version_checks
267
+ VersionChecks.all(extra_checks: Adapters.checks)
294
268
  end
295
269
 
296
270
  ##
297
- # Replaces safe code version literals in configured files.
271
+ # Fetches a registered version check.
298
272
  #
299
- # @return [void]
300
- def fix_code(args = nil)
301
- configuration, current_version = check_context
302
- target_version = target_version_argument(args, "semverve:fix:code")
303
- return report_current_target_noop(target_version) if target_version == current_version
304
-
305
- report_fix_results(
306
- [[
307
- "code version literal",
308
- VersionCodeReferences.new(configuration, current_version, target_version: target_version).fix
309
- ]],
310
- clean_message: "Code version literals are current."
311
- )
312
- end
313
-
314
- ##
315
- # Checks gem metadata for version mismatches.
273
+ # @param [Symbol, String] name
316
274
  #
317
- # @return [void]
318
- def check_metadata
319
- configuration, current_version = check_context
320
- report_findings(
321
- [[nil, VersionMetadata.new(configuration, current_version).findings]],
322
- current_version,
323
- clean_message: "Version metadata is current."
324
- )
275
+ # @return [#name]
276
+ def version_check(name)
277
+ VersionChecks.fetch(name, extra_checks: Adapters.checks)
325
278
  end
326
279
 
327
280
  ##
@@ -349,18 +302,6 @@ module Semverve
349
302
  puts "Release checks passed."
350
303
  end
351
304
 
352
- ##
353
- # Fixes safe gem metadata version mismatches.
354
- #
355
- # @return [void]
356
- def fix_metadata
357
- configuration, current_version = check_context
358
- report_fix_results(
359
- [["metadata version", VersionMetadata.new(configuration, current_version).fix]],
360
- clean_message: "Version metadata is current."
361
- )
362
- end
363
-
364
305
  ##
365
306
  # Resolved configuration and current version for check tasks.
366
307
  #
@@ -378,35 +319,21 @@ module Semverve
378
319
  #
379
320
  # @param [Semverve::SemanticVersion, nil] target_version
380
321
  #
381
- # @return [Array<Array(String, Array)>]
322
+ # @return [Array<Array(#name, String, Array)>]
382
323
  def check_groups(configuration, current_version, target_version = nil)
383
- groups = []
384
- if configuration.version_checks.include?(:doc_references)
385
- groups << [
386
- "version reference",
387
- VersionReferences.new(
388
- configuration,
389
- current_version,
390
- include_ignored: report_ignored?,
391
- target_version: target_version
392
- ).findings
393
- ]
394
- end
395
- if configuration.version_checks.include?(:code_references)
396
- groups << [
397
- "code version literal",
398
- VersionCodeReferences.new(
324
+ configuration.version_checks.map do |check_name|
325
+ version_check = version_check(check_name)
326
+ [
327
+ version_check,
328
+ version_check.finding_label,
329
+ version_check.findings(
399
330
  configuration,
400
331
  current_version,
401
332
  include_ignored: report_ignored?,
402
- target_version: target_version
403
- ).findings
333
+ target_version: target_version_for(version_check, target_version)
334
+ )
404
335
  ]
405
336
  end
406
- if configuration.version_checks.include?(:metadata)
407
- groups << [nil, VersionMetadata.new(configuration, current_version).findings]
408
- end
409
- groups
410
337
  end
411
338
 
412
339
  ##
@@ -486,6 +413,31 @@ module Semverve
486
413
  raise Error, "Run rake '#{task_name}[MAJOR.MINOR.PATCH]'."
487
414
  end
488
415
 
416
+ ##
417
+ # Optional exact version argument for checks that support targeting.
418
+ #
419
+ # @param [#targetable?] version_check
420
+ # @param [Rake::TaskArguments, nil] args
421
+ # @param [String] task_name
422
+ #
423
+ # @return [Semverve::SemanticVersion, nil]
424
+ def check_target_version(version_check, args, task_name)
425
+ return nil unless version_check.targetable?
426
+
427
+ target_version_argument(args, task_name)
428
+ end
429
+
430
+ ##
431
+ # Applies an umbrella target only to checks that support targeting.
432
+ #
433
+ # @param [#targetable?] version_check
434
+ # @param [Semverve::SemanticVersion, nil] target_version
435
+ #
436
+ # @return [Semverve::SemanticVersion, nil]
437
+ def target_version_for(version_check, target_version)
438
+ version_check.targetable? ? target_version : nil
439
+ end
440
+
489
441
  ##
490
442
  # Fix results enabled for the umbrella fix task.
491
443
  #
@@ -495,23 +447,17 @@ module Semverve
495
447
  #
496
448
  # @return [Array<Array(String, #replacement_count, #changed_files)>]
497
449
  def fix_results(configuration, current_version, target_version = nil)
498
- results = []
499
- if configuration.version_checks.include?(:doc_references)
500
- results << [
501
- "version reference",
502
- VersionReferences.new(configuration, current_version, target_version: target_version).fix
503
- ]
504
- end
505
- if configuration.version_checks.include?(:code_references)
506
- results << [
507
- "code version literal",
508
- VersionCodeReferences.new(configuration, current_version, target_version: target_version).fix
450
+ configuration.version_checks.map do |check_name|
451
+ version_check = version_check(check_name)
452
+ [
453
+ version_check.fix_label,
454
+ version_check.fix(
455
+ configuration,
456
+ current_version,
457
+ target_version: target_version_for(version_check, target_version)
458
+ )
509
459
  ]
510
460
  end
511
- if configuration.version_checks.include?(:metadata)
512
- results << ["metadata version", VersionMetadata.new(configuration, current_version).fix]
513
- end
514
- results
515
461
  end
516
462
 
517
463
  ##
@@ -525,8 +471,8 @@ module Semverve
525
471
  #
526
472
  # @return [void]
527
473
  def report_findings(groups, current_version, clean_message:, target_version: nil, fix_task_name: nil)
528
- findings = groups.flat_map do |(label, group_findings)|
529
- group_findings.map { |finding| [label || finding.label, finding] }
474
+ findings = groups.flat_map do |(version_check, label, group_findings)|
475
+ group_findings.map { |finding| [version_check, label || finding.label, finding] }
530
476
  end
531
477
 
532
478
  if findings.empty?
@@ -534,10 +480,10 @@ module Semverve
534
480
  return
535
481
  end
536
482
 
537
- findings.each do |(label, finding)|
483
+ findings.each do |(_version_check, label, finding)|
538
484
  puts "#{finding.path}:#{finding.line}:#{finding.column}: #{label} #{finding.version} -> #{current_version}"
539
485
  end
540
- if target_version == current_version && fix_task_name && reference_or_code_findings?(findings)
486
+ if target_version == current_version && fix_task_name && exact_target_fix_noop_findings?(findings)
541
487
  puts "Target version #{target_version} is already current; #{fix_task_name}[#{target_version}] will not change these references."
542
488
  end
543
489
 
@@ -556,15 +502,13 @@ module Semverve
556
502
  end
557
503
 
558
504
  ##
559
- # Whether findings include references controlled by exact-target fixes.
505
+ # Whether findings include surfaces controlled by exact-target fixes.
560
506
  #
561
- # @param [Array<Array(String, Object)>] findings
507
+ # @param [Array<Array(#exact_target_fix_noop_notice?, String, Object)>] findings
562
508
  #
563
509
  # @return [Boolean]
564
- def reference_or_code_findings?(findings)
565
- findings.any? do |(label, _finding)|
566
- ["version reference", "code version literal"].include?(label)
567
- end
510
+ def exact_target_fix_noop_findings?(findings)
511
+ findings.any? { |(version_check, _label, _finding)| version_check.exact_target_fix_noop_notice? }
568
512
  end
569
513
 
570
514
  ##
@@ -576,7 +520,7 @@ module Semverve
576
520
  # @return [void]
577
521
  def report_fix_results(results, clean_message: "Version checks passed.")
578
522
  replacement_count = results.sum { |(_label, result)| result.replacement_count }
579
- bundle_lock_ran = results.any? { |(_label, result)| result.respond_to?(:bundle_lock_ran) && result.bundle_lock_ran }
523
+ bundle_lock_ran = results.any? { |(_label, result)| result.bundle_lock_ran }
580
524
 
581
525
  if replacement_count.zero? && !bundle_lock_ran
582
526
  puts clean_message
@@ -15,7 +15,7 @@ module Semverve
15
15
  # Minor version.
16
16
  #
17
17
  # @return [Integer]
18
- MINOR = 3
18
+ MINOR = 4
19
19
 
20
20
  ##
21
21
  # Patch version.