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.
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler"
4
3
  require "rubygems"
5
4
 
5
+ require_relative "finding"
6
+ require_relative "fix_result"
6
7
  require_relative "semantic_version"
7
8
 
8
9
  module Semverve
9
10
  ##
10
- # Checks generated gem metadata against the configured version file.
11
- class VersionMetadata
11
+ # Checks generated package metadata against the configured version file.
12
+ class PackageMetadata
12
13
  ##
13
14
  # Literal gemspec version assignments that can be safely rewritten.
14
15
  #
@@ -16,117 +17,29 @@ module Semverve
16
17
  GEMSPEC_LITERAL_PATTERN = /^(\s*\w+\.version\s*=\s*)(["'])(\d+\.\d+\.\d+)(\2)/
17
18
 
18
19
  ##
19
- # A metadata version mismatch.
20
- class Finding
21
- ##
22
- # Path relative to the configured project root.
23
- #
24
- # @return [String]
25
- attr_reader :path
26
-
27
- ##
28
- # One-based line number.
29
- #
30
- # @return [Integer]
31
- attr_reader :line
32
-
33
- ##
34
- # One-based column number.
35
- #
36
- # @return [Integer]
37
- attr_reader :column
38
-
39
- ##
40
- # Metadata semantic version.
41
- #
42
- # @return [Semverve::SemanticVersion]
43
- attr_reader :version
44
-
45
- ##
46
- # Output label for the finding.
47
- #
48
- # @return [String]
49
- attr_reader :label
50
-
51
- ##
52
- # Initializes a finding.
53
- #
54
- # @param [String] path
55
- # @param [Integer] line
56
- # @param [Integer] column
57
- # @param [Semverve::SemanticVersion] version
58
- # @param [String] label
59
- #
60
- # @return [Semverve::VersionMetadata::Finding]
61
- def initialize(path:, line:, column:, version:, label:)
62
- @path = path
63
- @line = line
64
- @column = column
65
- @version = version
66
- @label = label
67
- end
68
- end
69
-
70
- ##
71
- # Result of fixing metadata version mismatches.
72
- class FixResult
73
- ##
74
- # Files changed by the fix.
75
- #
76
- # @return [Array<String>]
77
- attr_reader :changed_files
78
-
79
- ##
80
- # Number of literal replacements made.
81
- #
82
- # @return [Integer]
83
- attr_reader :replacement_count
84
-
85
- ##
86
- # Whether bundle lock was run.
87
- #
88
- # @return [Boolean]
89
- attr_reader :bundle_lock_ran
90
-
91
- ##
92
- # Initializes a fix result.
93
- #
94
- # @param [Array<String>] changed_files
95
- # @param [Integer] replacement_count
96
- # @param [Boolean] bundle_lock_ran
97
- #
98
- # @return [Semverve::VersionMetadata::FixResult]
99
- def initialize(changed_files:, replacement_count:, bundle_lock_ran:)
100
- @changed_files = changed_files
101
- @replacement_count = replacement_count
102
- @bundle_lock_ran = bundle_lock_ran
103
- end
104
- end
105
-
106
- ##
107
- # Initializes metadata version checking.
20
+ # Initializes package metadata version checking.
108
21
  #
109
22
  # @param [Semverve::ResolvedConfiguration] configuration
110
23
  # @param [Semverve::SemanticVersion] current_version
111
24
  #
112
- # @return [Semverve::VersionMetadata]
25
+ # @return [Semverve::PackageMetadata]
113
26
  def initialize(configuration, current_version)
114
27
  @configuration = configuration
115
28
  @current_version = current_version
116
29
  end
117
30
 
118
31
  ##
119
- # Metadata mismatches.
32
+ # Package metadata mismatches.
120
33
  #
121
- # @return [Array<Semverve::VersionMetadata::Finding>]
34
+ # @return [Array<Semverve::Finding>]
122
35
  def findings
123
36
  [gemspec_finding, lockfile_finding].compact
124
37
  end
125
38
 
126
39
  ##
127
- # Fixes safe metadata mismatches.
40
+ # Fixes safe package metadata mismatches.
128
41
  #
129
- # @return [Semverve::VersionMetadata::FixResult]
42
+ # @return [Semverve::FixResult]
130
43
  def fix
131
44
  changed_files = []
132
45
  replacement_count = fix_gemspec_literal
@@ -135,7 +48,7 @@ module Semverve
135
48
 
136
49
  bundle_lock_ran = lockfile_finding ? run_bundle_lock : false
137
50
 
138
- FixResult.new(
51
+ Semverve::FixResult.new(
139
52
  changed_files: changed_files,
140
53
  replacement_count: replacement_count,
141
54
  bundle_lock_ran: bundle_lock_ran
@@ -151,7 +64,7 @@ module Semverve
151
64
  attr_reader :configuration
152
65
 
153
66
  ##
154
- # Current gem version.
67
+ # Current package version.
155
68
  #
156
69
  # @return [Semverve::SemanticVersion]
157
70
  attr_reader :current_version
@@ -167,14 +80,14 @@ module Semverve
167
80
  ##
168
81
  # Finding for a gemspec mismatch.
169
82
  #
170
- # @return [Semverve::VersionMetadata::Finding, nil]
83
+ # @return [Semverve::Finding, nil]
171
84
  def gemspec_finding
172
85
  return unless gemspec_file && gemspec_version
173
86
  return if gemspec_version == current_version
174
87
 
175
88
  line, column = gemspec_position
176
89
 
177
- Finding.new(
90
+ Semverve::Finding.new(
178
91
  path: relative_path(gemspec_file),
179
92
  line: line,
180
93
  column: column,
@@ -186,7 +99,7 @@ module Semverve
186
99
  ##
187
100
  # Finding for a lockfile mismatch.
188
101
  #
189
- # @return [Semverve::VersionMetadata::Finding, nil]
102
+ # @return [Semverve::Finding, nil]
190
103
  def lockfile_finding
191
104
  return unless File.file?(lockfile_path)
192
105
 
@@ -196,7 +109,7 @@ module Semverve
196
109
 
197
110
  line, column = lockfile_position(version)
198
111
 
199
- Finding.new(
112
+ Semverve::Finding.new(
200
113
  path: "Gemfile.lock",
201
114
  line: line,
202
115
  column: column,
@@ -210,6 +123,8 @@ module Semverve
210
123
  #
211
124
  # @return [String, nil]
212
125
  def gemspec_file
126
+ return unless configuration.gem_name
127
+
213
128
  @gemspec_file ||= begin
214
129
  files = Dir.glob(File.join(root, "*.gemspec"))
215
130
 
@@ -271,18 +186,14 @@ module Semverve
271
186
  #
272
187
  # @return [Semverve::SemanticVersion, nil]
273
188
  def lockfile_version
274
- spec = lockfile_parser.specs.find { |candidate| candidate.name == configuration.gem_name }
275
- return unless spec
189
+ return unless configuration.gem_name
276
190
 
277
- SemanticVersion.parse(spec.version.to_s)
278
- end
191
+ File.readlines(lockfile_path).each do |line|
192
+ match = line.match(lockfile_line_pattern)
193
+ return SemanticVersion.parse(match[:version]) if match
194
+ end
279
195
 
280
- ##
281
- # Parsed Gemfile.lock.
282
- #
283
- # @return [Bundler::LockfileParser]
284
- def lockfile_parser
285
- Bundler::LockfileParser.new(File.read(lockfile_path))
196
+ nil
286
197
  end
287
198
 
288
199
  ##
@@ -292,16 +203,25 @@ module Semverve
292
203
  #
293
204
  # @return [Array(Integer, Integer)]
294
205
  def lockfile_position(version)
295
- pattern = /^\s*#{Regexp.escape(configuration.gem_name)}\s+\(#{Regexp.escape(version.to_s)}\)/
296
-
297
206
  File.readlines(lockfile_path).each_with_index do |line, index|
298
- match = line.match(pattern)
207
+ match = line.match(lockfile_line_pattern(version))
299
208
  return [index + 1, line.index(version.to_s) + 1] if match
300
209
  end
301
210
 
302
211
  [1, 1]
303
212
  end
304
213
 
214
+ ##
215
+ # Gemfile.lock line pattern for the configured package.
216
+ #
217
+ # @param [Semverve::SemanticVersion, nil] version
218
+ #
219
+ # @return [Regexp]
220
+ def lockfile_line_pattern(version = nil)
221
+ version_pattern = version ? Regexp.escape(version.to_s) : "\\d+\\.\\d+\\.\\d+"
222
+ /^\s*#{Regexp.escape(configuration.gem_name)}\s+\((?<version>#{version_pattern})\)/
223
+ end
224
+
305
225
  ##
306
226
  # Fixes a literal gemspec version assignment.
307
227
  #
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "error"
3
+ require_relative "adapters"
4
4
 
5
5
  module Semverve
6
6
  ##
7
- # Framework-specific default configuration adapters.
7
+ # Backward-compatible framework preset lookup.
8
8
  module Presets
9
9
  ##
10
10
  # Returns defaults for a configured preset.
@@ -14,9 +14,7 @@ module Semverve
14
14
  #
15
15
  # @return [Hash]
16
16
  def self.defaults_for(name, configuration)
17
- return {} unless name
18
-
19
- fetch(name).defaults(configuration)
17
+ Adapters.defaults_for(name, configuration)
20
18
  end
21
19
 
22
20
  ##
@@ -26,101 +24,7 @@ module Semverve
26
24
  #
27
25
  # @return [#name, #defaults]
28
26
  def self.fetch(name)
29
- case name.to_sym
30
- when :rails
31
- Rails.new
32
- else
33
- raise Error, "Unknown preset #{name.inspect}. Use :rails."
34
- end
35
- end
36
-
37
- ##
38
- # Rails application defaults.
39
- class Rails
40
- ##
41
- # Preset name.
42
- #
43
- # @return [Symbol]
44
- def name
45
- :rails
46
- end
47
-
48
- ##
49
- # Rails-specific Semverve defaults.
50
- #
51
- # @param [Semverve::Configuration] _configuration
52
- #
53
- # @return [Hash]
54
- def defaults(_configuration)
55
- root = rails_root || Dir.pwd
56
-
57
- {
58
- format: :simple,
59
- module_name: rails_module_name || project_module_name(root),
60
- root: root,
61
- version_file: File.join("config", "version.rb")
62
- }
63
- end
64
-
65
- private
66
-
67
- ##
68
- # Rails.root when Rails is available.
69
- #
70
- # @return [String, nil]
71
- def rails_root
72
- return unless rails_defined?
73
- return unless ::Rails.respond_to?(:root)
74
- return unless ::Rails.root
75
-
76
- ::Rails.root.to_s
77
- end
78
-
79
- ##
80
- # Application module name inferred from Rails.application.
81
- #
82
- # @return [String, nil]
83
- def rails_module_name
84
- return unless rails_defined?
85
- return unless ::Rails.respond_to?(:application)
86
- return unless ::Rails.application
87
-
88
- application_class = ::Rails.application.class
89
- return application_class.module_parent_name if application_class.respond_to?(:module_parent_name)
90
-
91
- class_name = application_class.name
92
- return unless class_name&.include?("::")
93
-
94
- class_name.split("::").first
95
- end
96
-
97
- ##
98
- # Module name fallback based on the project directory.
99
- #
100
- # @param [String] root
101
- #
102
- # @return [String]
103
- def project_module_name(root)
104
- camelize(File.basename(File.expand_path(root)))
105
- end
106
-
107
- ##
108
- # Whether Rails is loaded.
109
- #
110
- # @return [Boolean]
111
- def rails_defined?
112
- Object.const_defined?(:Rails)
113
- end
114
-
115
- ##
116
- # Converts a project directory into a Ruby module name.
117
- #
118
- # @param [String] value
119
- #
120
- # @return [String]
121
- def camelize(value)
122
- value.split(/[_-]/).map(&:capitalize).join
123
- end
27
+ Adapters.fetch(name)
124
28
  end
125
29
  end
126
30
  end
@@ -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