strong_versions 0.3.2 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9589edc74d736ee2c17217c9bf8c4efe316c8ada4842ecec28a16d27f939afa9
4
- data.tar.gz: 438bcc252b6bde7667b223b2efd55f26ca56597b21f5f238b9747ad78b1fb96b
3
+ metadata.gz: f73f7623edb5e3a662b18ea78cd67822dfdbcc81bd6eb0e9332e80bd1eda57c6
4
+ data.tar.gz: 9ed40edc5e76826e6458848fadd2b92a5b9f0985cefa0a3e56943c176e32edbb
5
5
  SHA512:
6
- metadata.gz: 1688a28f9c4c9fa03c78c99d59994508bfa1cec61005751aa2e0b32f5388cec5736557449ae072965ef6448c229e4527bcbee7f53581e2cf47fbf8df72ea8e2f
7
- data.tar.gz: aeae4848ecfb0ad258806e42ca4294d5010d0c7cbf5c434914eea8dc29214f1e9c7be11cd1c6c5de0c37683f2efad5ba88a110038ef84415566ed365970697d3
6
+ metadata.gz: 17fc0b16150395edf3d57daed7893803fd012311620285ad3a57df5330cfb28b1c1eebde904ece4484c5651c203dc27eeeb34f4699d79e6d443954020da23f81
7
+ data.tar.gz: 67d7d4dd885f70cf4877e72180311545606abc33d8191039c95c9afdcc2d2ef8a081222ab4fa56207e23e7830fa9adda0269444c093d8983e4c227119f9fe82a
data/Makefile CHANGED
@@ -3,3 +3,4 @@ test:
3
3
  @bin/rspec
4
4
  @bundle exec bin/strong_versions
5
5
  @bin/rubocop
6
+ @bin/rubocop bin/strong_versions
data/README.md CHANGED
@@ -28,7 +28,7 @@ The benefit of applying this standard is that, if all gems follow [Semantic Vers
28
28
  Add the gem to your `Gemfile`
29
29
 
30
30
  ```ruby
31
- gem 'strong_versions', '~> 0.3.2'
31
+ gem 'strong_versions', '~> 0.4.0'
32
32
  ```
33
33
 
34
34
  And rebuild your bundle:
@@ -39,7 +39,7 @@ $ bundle install
39
39
 
40
40
  Or install yourself:
41
41
  ```bash
42
- $ gem install strong_versions -v '0.3.2'
42
+ $ gem install strong_versions -v '0.4.0'
43
43
  ```
44
44
 
45
45
  ## Usage
@@ -54,6 +54,11 @@ The executable will output all non-passing gems and will return an exit code of
54
54
 
55
55
  ![StrongVersions](doc/images/ci-pipeline.png)
56
56
 
57
+ If you are feeling brave, auto-correct is available:
58
+ ```bash
59
+ $ bundle exec strong_versions -a
60
+ ```
61
+
57
62
  ### Exclusions
58
63
 
59
64
  <a name="ignore"></a>You can tell _StrongVersions_ to ignore any of your gems (e.g. those that don't follow _semantic versioning_) by adding them to the `ignore` section of `.strong_versions.yml` in your project root, e.g.:
data/bin/strong_versions CHANGED
@@ -1,14 +1,43 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require 'shellwords'
2
6
 
3
7
  require 'strong_versions'
4
8
 
9
+ original_args = ARGV.dup
10
+
11
+ options = {}
12
+ OptionParser.new do |opts|
13
+ opts.banner = 'Usage: strong_versions [options]'
14
+
15
+ opts.on('-a', '--auto-correct', 'Auto-correct (use with caution)') do |_v|
16
+ options[:auto_correct] = true
17
+ end
18
+
19
+ opts.on('--no-auto-correct', 'Disable auto-correct') do |_v|
20
+ options[:auto_correct] = false
21
+ end
22
+ end.parse!
23
+
24
+ def dependencies
25
+ StrongVersions::DependencyFinder.new.dependencies
26
+ end
27
+
5
28
  config_path = Bundler.root.join('.strong_versions.yml')
6
29
  config = StrongVersions::Config.new(config_path)
7
- dependencies = StrongVersions::DependencyFinder.new.dependencies
8
- valid = StrongVersions::Dependencies.new(dependencies).validate!(
30
+ validated = StrongVersions::Dependencies.new(dependencies).validate!(
9
31
  except: config.exceptions,
10
- on_failure: 'warn'
32
+ on_failure: 'warn',
33
+ auto_correct: options[:auto_correct]
11
34
  )
12
35
 
13
- exit 0 if valid
36
+ if options[:auto_correct]
37
+ # Re-evaluate
38
+ args = original_args.map { |arg| Shellwords.escape(arg) }.join(' ')
39
+ exec "#{$PROGRAM_NAME} #{args} --no-auto-correct"
40
+ end
41
+
42
+ exit 0 if validated
14
43
  exit 1
@@ -0,0 +1,35 @@
1
+ require 'optparse'
2
+
3
+ require 'strong_versions'
4
+
5
+ options = {}
6
+ OptionParser.new do |opts|
7
+ opts.banner = "Usage: strong_versions [options]"
8
+
9
+ opts.on("-a", "--auto-correct", "Auto-correct (use with caution)") do |v|
10
+ options[:auto_correct] = true
11
+ end
12
+ end.parse!
13
+
14
+ def dependencies
15
+ StrongVersions::DependencyFinder.new.dependencies
16
+ end
17
+
18
+ config_path = Bundler.root.join('.strong_versions.yml')
19
+ config = StrongVersions::Config.new(config_path)
20
+ validated = StrongVersions::Dependencies.new(dependencies).validate!(
21
+ except: config.exceptions,
22
+ on_failure: 'warn',
23
+ auto_correct: options[:auto_correct]
24
+ )
25
+
26
+ revalidated = false
27
+ revalidated = StrongVersions::Dependencies.new(dependencies).validate!(
28
+ except: config.exceptions,
29
+ on_failure: 'warn',
30
+ auto_correct: false
31
+ ) if options[:auto_correct]
32
+
33
+ exit 0 if validated or revalidated
34
+ exit 1
35
+
@@ -13,3 +13,4 @@ en:
13
13
  unknown_on_failure: "StrongVersions: Unknown value for `on_failure` in .strong_versions.yml: '%{on_failure}'. Expected one of %{expected}"
14
14
  suggested: 'Suggested: '
15
15
  version_not_specified: "[not specified]"
16
+ no-suggestion: "[unable to detect installed gem version]"
Binary file
@@ -9,6 +9,7 @@ require 'strong_versions/config'
9
9
  require 'strong_versions/dependency'
10
10
  require 'strong_versions/dependency_finder'
11
11
  require 'strong_versions/dependencies'
12
+ require 'strong_versions/errors'
12
13
  require 'strong_versions/suggestion'
13
14
  require 'strong_versions/terminal'
14
15
  require 'strong_versions/version'
@@ -11,19 +11,23 @@ module StrongVersions
11
11
  end
12
12
 
13
13
  def validate!(options = {})
14
+ auto_correct = options.delete(:auto_correct) { false }
14
15
  if validate(options)
15
16
  summary
16
17
  return true
17
18
  end
18
19
 
20
+ return update_gemfile if auto_correct
21
+
19
22
  raise_or_warn(options.fetch(:on_failure, 'raise'))
20
23
  summary
21
24
  false
22
25
  end
23
26
 
24
27
  def validate(options = {})
28
+ unsafe_autocorrect_error if options[:auto_correct]
25
29
  @dependencies.each do |dependency|
26
- next if options.fetch(:except).include?(dependency.name)
30
+ next if options.fetch(:except, []).include?(dependency.name)
27
31
  next if dependency.valid?
28
32
 
29
33
  @invalid_gems.push(dependency) unless dependency.valid?
@@ -33,10 +37,48 @@ module StrongVersions
33
37
 
34
38
  private
35
39
 
40
+ def unsafe_autocorrect_error
41
+ raise UnsafeAutoCorrectError, 'Must use #validate! for autocorrect'
42
+ end
43
+
36
44
  def summary
37
45
  @terminal.summary(@dependencies.size, @invalid_gems.size)
38
46
  end
39
47
 
48
+ def update_gemfile
49
+ updated = 0
50
+ @dependencies.each do |dependency|
51
+ next unless dependency.updatable?
52
+
53
+ updated += 1 if update_dependency(dependency)
54
+ end
55
+ @terminal.update_summary(updated)
56
+ end
57
+
58
+ def update_dependency(dependency)
59
+ path = dependency.gemfile
60
+ content = File.read(path)
61
+ update = replace_gem_definition(dependency, content)
62
+ return false if content == update
63
+
64
+ File.write(path, update)
65
+ @terminal.gem_update(path, dependency)
66
+ true
67
+ end
68
+
69
+ def replace_gem_definition(dependency, content)
70
+ regex = gem_regex(dependency.name)
71
+ match = content.match(regex)
72
+ return content unless match
73
+
74
+ indent = match.captures.first
75
+ content.gsub(regex, "#{indent}#{dependency.suggested_definition}")
76
+ end
77
+
78
+ def gem_regex(name)
79
+ /^(\s*)gem\s+['"]#{name}['"].*$/
80
+ end
81
+
40
82
  def raise_or_warn(on_failure)
41
83
  case on_failure
42
84
  when 'raise'
@@ -10,9 +10,11 @@ module StrongVersions
10
10
  @errors = []
11
11
  @lockfile = lockfile || default_lockfile
12
12
 
13
- versions.each do |operator, version|
14
- validate_version(operator, version)
15
- end
13
+ versions.each { |operator, version| validate_version(operator, version) }
14
+ end
15
+
16
+ def gemfile
17
+ Pathname.new(@dependency.gemfile) if @dependency.respond_to?(:gemfile)
16
18
  end
17
19
 
18
20
  def valid?
@@ -23,16 +25,31 @@ module StrongVersions
23
25
  Suggestion.new(lockfile_version)
24
26
  end
25
27
 
28
+ def suggested_definition
29
+ guards = guard_versions.map { |op, version| "'#{op} #{version}'" }
30
+ "gem '#{@name}', #{[suggestion, *guards].join(', ')}"
31
+ end
32
+
26
33
  def definition
27
- versions.map { |operator, version| "'#{operator} #{version}'" }.join(', ')
34
+ versions.map do |operator, version|
35
+ next t('version_not_specified') if operator == '>=' && version == '0'
36
+
37
+ "'#{operator} #{version}'"
38
+ end.join(', ')
39
+ end
40
+
41
+ def updatable?
42
+ gemfile && !suggestion.missing? && !path_source?
28
43
  end
29
44
 
30
45
  private
31
46
 
32
47
  def versions
33
- @dependency.requirements_list.map do |requirement|
34
- parse_version(requirement)
35
- end
48
+ @dependency.requirements_list.map { |version| parse_version(version) }
49
+ end
50
+
51
+ def guard_versions
52
+ versions.reject { |op, version| redundant?(op, version) }
36
53
  end
37
54
 
38
55
  def parse_version(requirement)
@@ -76,14 +93,25 @@ module StrongVersions
76
93
  def check_valid_version(version)
77
94
  return if valid_version?(version)
78
95
 
79
- value = if version == '0'
80
- I18n.t('strong_versions.version_not_specified')
81
- else
82
- version
83
- end
96
+ value = version == '0' ? t('version_not_specified') : version
84
97
  @errors << { type: :version, value: value }
85
98
  end
86
99
 
100
+ def redundant?(operator, version)
101
+ return false unless operator.start_with?('>') || pessimistic?(operator)
102
+
103
+ multiply_version(version) <= multiply_version(suggestion.version)
104
+ end
105
+
106
+ def multiply_version(version)
107
+ # Support extremely precise versions e.g. '1.2.3.4.5.6.7.8.9'
108
+ components = version.split('.').map(&:to_i)
109
+ components += [0] * (10 - components.size)
110
+ components.reverse.each_with_index.map do |component, index|
111
+ component * 10.pow(index + 1)
112
+ end.sum
113
+ end
114
+
87
115
  def pessimistic?(operator)
88
116
  operator == '~>'
89
117
  end
@@ -107,14 +135,8 @@ module StrongVersions
107
135
  @dependency.source.is_a?(Bundler::Source::Path)
108
136
  end
109
137
 
110
- def pessimistic_with_upper_bound?(operator)
111
- any_pessimistic? && %w[< <=].include?(operator)
112
- end
113
-
114
- def any_pessimistic?
115
- versions.any? do |_version, operator|
116
- %w[< <= ~>].include?(operator)
117
- end
138
+ def t(name)
139
+ I18n.t("strong_versions.#{name}")
118
140
  end
119
141
  end
120
142
  end
@@ -10,6 +10,7 @@ module StrongVersions
10
10
 
11
11
  def development
12
12
  # Gem runtime dependencies are not included here:
13
+ Bundler.definition.resolve
13
14
  Bundler.definition.dependencies
14
15
  end
15
16
 
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrongVersions
4
+ class Error < StandardError; end
5
+ class UnsafeAutoCorrectError < Error; end
6
+ end
@@ -3,7 +3,11 @@
3
3
  module StrongVersions
4
4
  class Suggestion
5
5
  def initialize(version)
6
- @parts = version.split('.') unless version.nil?
6
+ return if version.nil?
7
+
8
+ @parts = version.split('.')
9
+ # Treat '4.3.2.1' as '4.3.2'
10
+ @parts.pop if standard?(@parts.first(3)) && @parts.size == 4
7
11
  end
8
12
 
9
13
  def to_s
@@ -12,6 +16,16 @@ module StrongVersions
12
16
  "'~> #{version}'"
13
17
  end
14
18
 
19
+ def version
20
+ return nil unless standard?
21
+
22
+ major, minor, patch = @parts
23
+ return "#{major}.#{minor}" if stable?
24
+ return "#{major}.#{minor}.#{patch}" if unstable?
25
+
26
+ raise 'Unexpected condition met'
27
+ end
28
+
15
29
  def missing?
16
30
  return false if stable?
17
31
  return false if unstable?
@@ -21,15 +35,6 @@ module StrongVersions
21
35
 
22
36
  private
23
37
 
24
- def version
25
- major, minor, patch = @parts if standard?
26
-
27
- return "#{major}.#{minor}" if stable?
28
- return "#{major}.#{minor}.#{patch}" if unstable?
29
-
30
- nil
31
- end
32
-
33
38
  def unstable?
34
39
  standard? && @parts.first.to_i.zero?
35
40
  end
@@ -38,12 +43,13 @@ module StrongVersions
38
43
  standard? && @parts.first.to_i >= 1
39
44
  end
40
45
 
41
- def standard?
42
- return false if @parts.nil?
43
- return false unless @parts.size == 3
46
+ def standard?(parts = @parts)
47
+ return false if parts.nil?
44
48
  return false unless numeric?
49
+ return true if [2, 3].include?(parts.size)
50
+ return true if parts.size == 3 && unstable?
45
51
 
46
- true
52
+ false
47
53
  end
48
54
 
49
55
  def numeric?
@@ -7,7 +7,27 @@ module StrongVersions
7
7
  end
8
8
 
9
9
  def warn(string)
10
- puts(color(string, :underline, :bright, :red))
10
+ puts(color(string, :bright, :red))
11
+ end
12
+
13
+ def gem_update(path, gem)
14
+ relpath = path.relative_path_from(Pathname.new(Dir.pwd))
15
+ output = [
16
+ color("[#{relpath}] ", :cyan),
17
+ color(gem.suggested_definition, :green),
18
+ color(' (was: ', :default),
19
+ color(gem.definition, :red),
20
+ color(')', :default)
21
+ ].join
22
+ puts(output)
23
+ end
24
+
25
+ def update_summary(updated)
26
+ output = [
27
+ "#{updated} gem definitions ",
28
+ color('updated', :green)
29
+ ].join
30
+ puts("\n#{output}")
11
31
  end
12
32
 
13
33
  def summary(count, failed)
@@ -67,13 +87,15 @@ module StrongVersions
67
87
 
68
88
  def suggestion(gem)
69
89
  suggested = ' ' + t('errors.suggested')
70
- puts(
71
- color(
72
- "#{suggested} %{suggestion}",
73
- :default,
74
- suggestion: [gem.suggestion.to_s, :green]
75
- )
76
- )
90
+ puts(color("#{suggested}%{suggestion}", :default,
91
+ suggestion: suggestion_definition(gem)))
92
+ end
93
+
94
+ def suggestion_definition(gem)
95
+ unidentified = gem.suggestion.to_s.empty?
96
+ return [t('no-suggestion'), :yellow] if unidentified
97
+
98
+ [gem.suggestion.to_s, :green]
77
99
  end
78
100
 
79
101
  def name_and_definition(gem)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StrongVersions
4
- VERSION = '0.3.2'
4
+ VERSION = '0.4.0'
5
5
  end
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.name = 'strong_versions'
9
9
  spec.version = StrongVersions::VERSION
10
10
  spec.authors = ['Bob Farrell']
11
- spec.email = ['robertanthonyfarrell@gmail.com']
11
+ spec.email = ['oss@bob.frl']
12
12
 
13
13
  spec.summary = 'Enforce strict versioning on your Gemfile'
14
14
  spec.description = 'Ensure your gems are appropriately versioned'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strong_versions
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-25 00:00:00.000000000 Z
11
+ date: 2020-01-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: i18n
@@ -138,7 +138,7 @@ dependencies:
138
138
  version: 0.60.0
139
139
  description: Ensure your gems are appropriately versioned
140
140
  email:
141
- - robertanthonyfarrell@gmail.com
141
+ - oss@bob.frl
142
142
  executables:
143
143
  - strong_versions
144
144
  extensions: []
@@ -161,6 +161,7 @@ files:
161
161
  - bin/rubocop
162
162
  - bin/setup
163
163
  - bin/strong_versions
164
+ - bin/strong_versions.rb
164
165
  - config/locales/en.yml
165
166
  - doc/images/ci-pipeline.png
166
167
  - doc/images/strong-versions-example.png
@@ -169,6 +170,7 @@ files:
169
170
  - lib/strong_versions/dependencies.rb
170
171
  - lib/strong_versions/dependency.rb
171
172
  - lib/strong_versions/dependency_finder.rb
173
+ - lib/strong_versions/errors.rb
172
174
  - lib/strong_versions/suggestion.rb
173
175
  - lib/strong_versions/terminal.rb
174
176
  - lib/strong_versions/version.rb