deprecool 0.1.2 → 0.1.4

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: 959b5cdb23ec0d5eef80f87185fb7a31a5646cf991cced0030d4d839fb835906
4
- data.tar.gz: 87eac39b80c35af2e3bc868ab99b03d534f46f06e0f6ef6945381e5d00e8798b
3
+ metadata.gz: 354619cb58b3d0b43f1aaffbd90720589a350d8d31f9f52a7c8cab5db4ee8125
4
+ data.tar.gz: b9ae44fa91e1c5c7815902810596f82c115981f35982ac3c1bedc89b78f8d7ce
5
5
  SHA512:
6
- metadata.gz: d6da4e681409697beeaad714e669ad6854000a66a9086a8bf593ded61016e3741beb5147d616a42d99c3e19b3d51ad886f432d056ba58db2e56a559b0188ae87
7
- data.tar.gz: cd41a34a6787fb74f7985e9180893d46a765a19d369b90c09d23738a378ee645bce9d02109fda4e7806195f82bc94f9b0d2ac300ec6c69bf6d0ccb65c9e41e9a
6
+ metadata.gz: 84eb84064dfa7230addf8dec40fad61affc9050747f22b3dc2e938e9545a20d58f6d42d567b7c85189d4421d52f57a2a42572eee36fef5624fb46da77a8eee6e
7
+ data.tar.gz: fc44486aab2d15112a99f04136dda227896a728f9e75032fd2450d18471904d60469538fc6714e4350d3621dedeac9770bde1b8d5474e4609da62116ba326f52
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Deprecool
2
2
 
3
+ ## [0.1.4] - 2026-09-20
4
+ ### deprecations
5
+ - Rails 8.2.0 deprecation of ActiveRecord#uniq!
6
+ - Ruby 4.0's deprecation of the limit argument to JSON.dump
7
+
8
+ ### gem internals
9
+ - Finder Test Helper change from `assert_no_offense` to `assert_no_offenses`
10
+ - auto-inclusion of a gem's helper method class to Finders under a gem's namespace
11
+ - support gemspec parsing
12
+ - better file finding and error handling
13
+
14
+ ## [0.1.3] - 2026-08-24
15
+ - Added Finder for: "Passing `binds` as a positional argument to `insert` deprecated", see [original commit](https://github.com/rails/rails/commit/2dca5457ab2097626481fdbec233ea56d3fb9ee3)
16
+ - Added `effort` to the CLI output
17
+
3
18
  ## [0.1.2] - 2026-08-14
4
19
 
5
20
  - Add Finder for Rails's deprecation of `write_attribute(:id, value)`
data/lib/deprecool/cli.rb CHANGED
@@ -24,7 +24,7 @@ module Deprecool
24
24
  option :gems, type: :array, desc: 'Specify gem name(s) to list which versions have deprecation tracking available'
25
25
 
26
26
  def call(gems:)
27
- CLI.list_finders(Finder.registry)
27
+ CLI.list_finders(Finder.subclasses)
28
28
 
29
29
  # TODO: REPL to filter through the available finders?
30
30
  end
@@ -34,38 +34,57 @@ module Deprecool
34
34
  desc 'Scan files or directories for known deprecations'
35
35
 
36
36
  argument :paths, type: :array, desc: 'Files or directories to scan'
37
- option :lockfile, default: 'Gemfile.lock', desc: 'Path to Gemfile.lock, defaults to current directory'
38
- option :gems, type: :array, desc: 'Gems to scan for: --gems=ruby,rails'
39
- option :format, default: 'text', values: %w[text json], desc: 'Output format'
40
- option :all, type: :boolean, default: false, desc: 'Run every finder regardless of version'
41
37
 
42
- def call(paths:, format:, gems: [], all:, lockfile:, **)
38
+ option :format,
39
+ default: 'text',
40
+ values: %w[text json],
41
+ desc: 'Output format'
42
+
43
+ option :gems,
44
+ type: :array,
45
+ desc: 'Gems to scan for: --gems=ruby,rails'
46
+
47
+ option :lockfile,
48
+ default: 'Gemfile.lock',
49
+ desc: 'Path to Gemfile.lock, defaults to current directory'
50
+
51
+ option :all,
52
+ type: :boolean,
53
+ default: false,
54
+ desc: 'Run every finder regardless of version'
55
+
56
+ def call(paths:, format:, gems: [], lockfile:, all:, **)
43
57
  json_output = format == 'json'
44
- files = CLI.ruby_files(paths)
58
+ files = CLI.ruby_files(paths)
45
59
 
46
- if files.empty?
47
- if paths.empty?
48
- warn CLI.colorize('deprecool: please supply a path/to/file/or/directory', :red)
49
- exit 2
50
- end
60
+ if paths.empty?
61
+ puts 'No path given, using \'.\' as the path' unless json_output
62
+ end
51
63
 
52
- warn CLI.colorize("deprecool: no Ruby files found in #{paths.join(', ')}", :red)
53
- exit 2
64
+ # TODO: detect when deprecool is run on too many projects at once
65
+ # i.e. if you run it on a ~/Documents folder and it finds
66
+ # a ton of files from different projects it doesn't know what finders
67
+ # to use and finds nothing
68
+ if files.empty?
69
+ warn "No ruby files found in #{paths.join(', ')}"
70
+ exit 1
54
71
  end
55
72
 
56
- if gems.any?
57
- puts "Scanning for deprecations from: #{gems.join(', ')}" unless json_output
58
- # we won't be scanning a lockfile
59
- gem_versions = []
60
- elsif all
61
- gem_versions = []
62
- gems = []
63
- else
64
- puts "Scanning #{lockfile}..." unless json_output
65
- gem_versions = LockfileParser.parse!(lockfile)
66
-
67
- # we don't need to look for individual gems when scanning gemfiles
68
- gems = []
73
+ dependency_files = CLI.find_dependency_files
74
+ gem_versions = []
75
+ gems ||= []
76
+
77
+ # if the user supplied "all" then we don't worry about finding gem versions
78
+ if !all
79
+ # next we need to find where the dependencies come from, or we could match
80
+ # offenses from not applicable versions of gems
81
+ if dependency_files.any?
82
+ puts "Scanning #{dependency_files.join(', ')} ..."
83
+ gem_versions = DependencyParser.parse_all(dependency_files)
84
+ if gems.any?
85
+ puts "Scanning for deprecations from: #{gems.join(', ')}" unless json_output
86
+ end
87
+ end
69
88
  end
70
89
 
71
90
  finders = Registry.applicable(gems:, gem_versions:, include_all: all)
@@ -92,17 +111,28 @@ module Deprecool
92
111
 
93
112
  def ruby_files(paths)
94
113
  paths = paths.empty? ? ['.'] : paths
114
+ begin
115
+ files = paths.flat_map do |path|
116
+ if File.directory?(path)
117
+ Dir[File.join(path, '**', '*.rb')]
118
+ elsif File.file?(path)
119
+ [path]
120
+ else
121
+ warn "deprecool: no such file or directory: #{path}"
122
+ []
123
+ end
124
+ end.uniq.sort
125
+ rescue SystemCallError => err
126
+ warn "deprecool: An error occurred - #{err}"
127
+ ensure
128
+ files ||= []
95
129
 
96
- paths.flat_map do |path|
97
- if File.directory?(path)
98
- Dir[File.join(path, '**', '*.rb')]
99
- elsif File.file?(path)
100
- [path]
101
- else
102
- warn "deprecool: no such file or directory: #{path}"
103
- []
104
- end
105
- end.uniq.sort
130
+ return files
131
+ end
132
+ end
133
+
134
+ def find_dependency_files
135
+ Dir["*.gemspec", "Gemfile.lock"]
106
136
  end
107
137
 
108
138
  def report(offenses, format, finders)
@@ -116,7 +146,7 @@ module Deprecool
116
146
  def text_report(offenses, finders)
117
147
  if offenses.empty?
118
148
  puts colorize('No deprecations found.', :green)
119
- puts "(#{finders.size} finder#{'s' unless finders.size == 1} active)"
149
+ puts "(#{finders&.size || 0} finder(s) active)"
120
150
  return
121
151
  end
122
152
 
@@ -125,10 +155,13 @@ module Deprecool
125
155
  offenses.each_value do |offense_array|
126
156
  offense = offense_array.first
127
157
 
128
- puts "\n#{colorize(offense.title, :bold)}\n"
158
+ puts "\n#{offense.gem} #{offense.deprecated_in} - #{colorize(offense.title, :bold)}"
159
+ puts
129
160
  puts " * #{offense.summary}"
130
- puts " #{colorize('fix:', :green)} #{offense.suggestion}" if offense.suggestion
131
- puts " #{colorize('source:', :green)} #{offense.reference}" if offense.reference
161
+ puts
162
+ puts " #{colorize('effort:', :green).ljust(19)} #{colorize_effort(offense.effort)}"
163
+ puts " #{colorize('fix:', :green).ljust(19)} #{offense.suggestion}" if offense.suggestion
164
+ puts " #{colorize('source(s):', :green).ljust(19)} #{offense.reference}" if offense.reference
132
165
  puts ' Found At:'
133
166
  offense_array.each do |o|
134
167
  offense_count += 1
@@ -147,8 +180,8 @@ module Deprecool
147
180
 
148
181
  def list_finders(finders)
149
182
  puts 'Active finders:'
150
- finders.sort_by { [it.gem.to_s, it.deprecated_in.to_s, it.id.to_s] }.each do
151
- puts " - #{it.classname.ljust(35)} (#{it.gem} #{it.deprecated_in.to_s}) — #{it.title}"
183
+ finders.sort_by { |f| [f.gem.to_s, f.deprecated_in.to_s, f.id.to_s] }.each do
184
+ puts " - #{f.classname.ljust(35)} (#{f.gem} #{f.deprecated_in.to_s}) — #{f.title}"
152
185
  end
153
186
  puts ' (none)' if finders.empty?
154
187
  end
@@ -158,6 +191,17 @@ module Deprecool
158
191
 
159
192
  "\e[#{colors.fetch(color, nil)}m#{text}\e[0m"
160
193
  end
194
+
195
+ def colorize_effort(effort)
196
+ # the effort method raises if you set anything but these
197
+ # values so it's safe to assume, it would also be a hassle
198
+ # to remove duplication here so oh well
199
+ effort_colors = { low: :green,
200
+ medium: :yellow,
201
+ high: :red }
202
+
203
+ colorize(effort, effort_colors[effort])
204
+ end
161
205
  end
162
206
  end
163
207
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dependency_parsers/gemspec_parser'
4
+ require_relative 'dependency_parsers/lockfile_parser'
5
+ require_relative 'dependency_parsers/null_parser'
6
+
7
+ module Deprecool
8
+ module DependencyParser
9
+ extend self
10
+
11
+ def parse_all(dependency_files)
12
+ files = Array(dependency_files)
13
+
14
+ dependencies = GemVersions.new
15
+
16
+ files.each do |file|
17
+ dependencies.add_all!(parser_for_file(file).parse!(file))
18
+ end
19
+
20
+ dependencies
21
+ end
22
+
23
+ def parser_for_file(dependency_file)
24
+ case File.extname(dependency_file)
25
+ when '.lock' then DependencyParsers::LockfileParser
26
+ when '.gemspec' then DependencyParsers::GemspecParser
27
+ else
28
+ DependencyParsers::NullParser
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deprecool
4
+ module DependencyParsers
5
+ module GemspecParser
6
+ extend self
7
+
8
+ def parse!(gemspec_file_path)
9
+ raise FileNotFound.new('.gemspec') unless File.exist? gemspec_file_path
10
+
11
+ parsed = Gem::Specification.load(gemspec_file_path)
12
+
13
+ raise InvalidGemspec.new(gemspec_file_path) if parsed.nil?
14
+
15
+ gem_versions = GemVersions.new
16
+
17
+ parsed.dependencies.each do |dep|
18
+ gem_versions.add(gem: dep.name, version: minimum_version(dep.requirement))
19
+ end
20
+
21
+ gem_versions.add(gem: 'ruby', version: minimum_version(parsed.required_ruby_version))
22
+
23
+ gem_versions.gem_versions
24
+ end
25
+
26
+ private
27
+
28
+ def minimum_version(requirement)
29
+ lower_bounds = requirement.requirements.filter_map { |req| lower_bound(*req) }
30
+
31
+ lower_bounds.min || Gem::Version.new(0)
32
+ end
33
+
34
+ def lower_bound(operator, version)
35
+ case operator
36
+ when '=', '>=', '>', '~>' then version
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+
5
+ module Deprecool
6
+ module DependencyParsers
7
+ module LockfileParser
8
+ extend self
9
+
10
+ def parse!(path_to_gemfile_lock = 'Gemfile.lock', parser: Bundler::LockfileParser)
11
+ raise FileNotFound.new('.lock') unless File.exist? path_to_gemfile_lock
12
+
13
+ parsed = parser.new(File.read(path_to_gemfile_lock))
14
+ lockfile_gem_versions = GemVersions.new
15
+
16
+ parsed.specs.map do |spec|
17
+ lockfile_gem_versions.add(gem: spec.name, version: spec.version)
18
+ end
19
+
20
+ if parsed.ruby_version
21
+ gem, version = parsed.ruby_version.split
22
+ lockfile_gem_versions.add(gem:, version: without_patchlevel(version))
23
+ else
24
+ puts "No Ruby version found"
25
+ end
26
+
27
+ lockfile_gem_versions.gem_versions
28
+ end
29
+
30
+ private
31
+
32
+ def without_patchlevel(version) = version[/\d+(\.\d+)*/]
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deprecool
4
+ module DependencyParsers
5
+ module NullParser
6
+ extend self
7
+
8
+ def parse!(file)
9
+ warn "Attempted to parse #{file} for dependencies, but deprecool does not support #{File.extname(file)} yet."
10
+
11
+ []
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ # lib/errors.rb
2
+
3
+ module Deprecool
4
+ class Error < StandardError; end
5
+
6
+ class FileNotFound < Error
7
+ def initialize(filetype)
8
+ super("No ruby files found in '#{filetype}'")
9
+ end
10
+ end
11
+
12
+ class InvalidGemspec < Error
13
+ def initialize(path)
14
+ super("Could not load a gem specification from '#{path}'")
15
+ end
16
+ end
17
+
18
+ class NoPathGiven < Error
19
+ def initialize(_msg = nil)
20
+ super("No path given, and no ruby files found under '.'")
21
+ end
22
+ end
23
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'helpers/prism_helpers'
4
-
5
3
  module Deprecool
6
4
  # Base class for every deprecation finder.
7
5
  #
@@ -31,15 +29,13 @@ module Deprecool
31
29
  class Finder
32
30
  include PrismHelpers
33
31
 
34
- @registry = []
35
-
36
32
  class << self
37
- # All finder subclasses, in definition order.
38
- attr_reader :registry
39
-
33
+ # automatically include GemHelpers into the classes for the Gem
40
34
  def inherited(subclass)
41
- Finder.registry << subclass
42
- super
35
+ gem_name = subclass.name.split("::")[2]
36
+ return if gem_name == "Ruby"
37
+
38
+ subclass.send(:include, const_get("#{gem_name}Helpers"))
43
39
  end
44
40
 
45
41
  # defines class methods that set instance variables
@@ -130,9 +126,11 @@ module Deprecool
130
126
 
131
127
  # Record a deprecation at the given node's location.
132
128
  #
133
- # confidence - [:high, :low], but currently the only check for this is
134
- # from the CLI outputting red or yellow depending on confidence
129
+ # confidence - [:high, :low, :none], returns early if :none
130
+ # to save each finder the effort of checking for :none
135
131
  def add_offense(node, confidence: :high)
132
+ return if confidence == :none
133
+
136
134
  location = node.location # a Prism::Location
137
135
 
138
136
  @offenses << Offense.new(
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deprecool
4
+ module Finders
5
+ module Rails
6
+ module V8_2_0
7
+ class ActiveRecordRelationUniq < Deprecool::Finder
8
+ gem :rails
9
+ deprecated_in '8.2.0'
10
+ removed_in '9.0.0'
11
+ title 'ActiveRecord::Relation#uniq! is deprecated'
12
+ summary 'Since Rails 6.1 you have been able to use Relation#uniq! to remove duplicate fields ' \
13
+ 'on a query. This was originally because grouping by the same field twice resulted in ' \
14
+ 'duplicated results. Since Rails 7.0 though, this has been de-duplicated automatically ' \
15
+ 'and this method has been a near no-op'
16
+ suggestion 'Remove the call to #uniq!, don\'t just find and replace though ' \
17
+ 'because `uniq!` is also an Array method. Make sure you have tests ' \
18
+ 'backing these changes, as always.'
19
+ reference 'uniq! added in: https://github.com/rails/rails/pull/39358, ' \
20
+ 'uniq! deprecated in: https://github.com/rails/rails/pull/58525'
21
+ effort :low
22
+
23
+ def on_call_node(node)
24
+ return unless node.name == :uniq!
25
+ return unless args = unwrap_arguments(node.arguments)
26
+
27
+ # neither Array.uniq! or ActiveRecord's uniq! take
28
+ # more than one argument,
29
+ #
30
+ # Array only accepts an optional block
31
+ # AR receives a string or symbol
32
+ return unless args.count == 1
33
+
34
+ confidence = :none
35
+ confidence = :high if [Prism::SymbolNode, Prism::StringNode].include?(args.first.class)
36
+
37
+ # if uniq! is called on a variable like:
38
+ # some_query.uniq!(:somehting)
39
+ # then it's still probably a deprecation, but we are a little less sure
40
+ confidence = :low if receiver_is_bare_variable(node.receiver)
41
+
42
+
43
+ add_offense(node, confidence:)
44
+ end
45
+
46
+ def receiver_is_bare_variable(receiver)
47
+ receiver.is_a?(Prism::CallNode) && receiver.variable_call?
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deprecool
4
+ module Finders
5
+ module Rails
6
+ module V8_2_0
7
+ class InsertUpdateDeleteBinds < Deprecool::Finder
8
+ gem :rails
9
+ deprecated_in '8.2.0'
10
+ removed_in '9.0.0'
11
+ title 'Binds being bassed to Insert Update Delete is deprecated'
12
+ summary ' Now that `Arel.sql(sql_with_placeholders, *binds)` wraps SQL and its binds ' \
13
+ 'together as an `Arel::Nodes::BoundSqlLiteral`, just like ' \
14
+ '`Model.where("... = ?", value)`, the separate positional is no longer needed'
15
+ suggestion 'Old: `connection.insert("INSERT INTO topics (title) VALUES (?)", nil, nil, ["hello"])` ' \
16
+ 'New: `connection.insert(Arel.sql("INSERT INTO topics (title) VALUES (?)", "hello"))`'
17
+ reference 'Original Commit: https://github.com/rails/rails/commit/2dca5457ab2097626481fdbec233ea56d3fb9ee3'
18
+ effort :low
19
+
20
+ def on_call_node(node)
21
+ return unless %i[insert update delete].include?(node.name)
22
+ return if node.receiver.nil? # bare `delete :foo, on: :member` in a routes file, etc.
23
+ return unless arguments = unwrap_arguments(node.arguments)
24
+ return unless arguments.length > 1
25
+
26
+ confidence = confidence_from_arguments(arguments)
27
+
28
+ add_offense(node, confidence: confidence)
29
+ end
30
+
31
+ private
32
+
33
+ def confidence_from_arguments(arguments)
34
+ first = arguments[0]
35
+
36
+ # if first argument is string node that starts with "INSERT" "UPDATE" "DELETE" then :high
37
+ # if first argument is a variable then :low confidence
38
+ # it it's a Prism::CallNode, and not a variable call, then I don't know what it is here,
39
+ # maybe it's like 'connection.update(generate_sql(some_arg), ["name"])' and if you are doing that
40
+ # then good luck?
41
+ case first
42
+ when Prism::CallNode
43
+ # can't tell what's stored in the variable, https://docs.ruby-lang.org/en/master/Prism/CallNode.html#method-i-variable_call-3F
44
+ first.variable_call? ? :low : :none
45
+ when Prism::StringNode
46
+ first.unescaped.match?(/\A\s*(INSERT|UPDATE|DELETE)\s/i) ? :high : :none
47
+ else
48
+ :none
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deprecool
4
+ module Finders
5
+ module Ruby
6
+ module V4_0_0
7
+ class JsonDumpLimit < Deprecool::Finder
8
+ gem :ruby
9
+ deprecated_in '4.0.0'
10
+ removed_in '4.1.0'
11
+ title 'Passing a positional limit argument to JSON.dump is deprecated'
12
+ summary 'JSON.dump(obj, io, limit) accepted a nesting limit as a positional argument. ' \
13
+ 'Parsing that argument is awful and costly, so it is now named _deprecated_limit ' \
14
+ 'instead of fully removing because updating rdoc is costly.'
15
+ suggestion 'Pass the :max_nesting option instead, e.g. JSON.dump(obj, io, max_nesting: 10)'
16
+ reference 'https://github.com/ruby/json/commit/566bd7c108 ' \
17
+ 'https://github.com/ruby/ruby/commit/d98d7978dc0dd777756ba737295d9cec482f84a3'
18
+ effort :low
19
+
20
+ def on_call_node(node)
21
+ # we only care if the call is JSON.dump
22
+ return unless node.name == :dump
23
+ return unless json_receiver?(node.receiver)
24
+
25
+ # bare keyword options and blocks aren't positional arguments
26
+ positional = positional_args_from(node.arguments)
27
+
28
+ # :high because the receiver is JSON and the limit is only detected when it
29
+ # can't be anything else (a non-nil, non-hash third argument or an integer in place of io)
30
+ add_offense(node, confidence: :high) if passes_limit_arg?(positional)
31
+ end
32
+
33
+ private
34
+
35
+ def json_receiver?(receiver)
36
+ case receiver
37
+ when Prism::ConstantReadNode then receiver.name == :JSON
38
+ # ::JSON.dump
39
+ when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :JSON
40
+ else false
41
+ end
42
+ end
43
+
44
+ # follows the same steps JSON.dump uses to decide which argument is the limit
45
+ def passes_limit_arg?(positional)
46
+ # the positional param will dereference into all 3 variables only if
47
+ # all three are actually passed.
48
+ _obj, io, limit = positional
49
+
50
+ # if a third argument is passed, but it's a {hash} or nil
51
+ # then set the third arg to nil in case the second argument is being used
52
+ # as the limit i.e. (JSON.dump(obj, 1, strict: true))
53
+ limit = nil if limit.is_a?(Prism::HashNode) || limit.is_a?(Prism::NilNode)
54
+
55
+ # in the case the third arg was nil or a hash/nil then check if the second arg
56
+ # is an integer, this is similar to what the `dump` method does in ruby 4
57
+ limit = io if limit.nil? && integer?(io)
58
+
59
+
60
+ !limit.nil?
61
+ end
62
+
63
+ def integer?(node) = node.is_a?(Prism::IntegerNode)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -27,9 +27,9 @@ module Deprecool
27
27
  # I don't think we need much more than this
28
28
  # I'm open to be wrong though, maybe lots of people are subclassing
29
29
  # ObjectSpace or defining '_id2ref' on custom classes
30
- confidence = node.receiver.name == :ObjectSpace ? :high : nil
30
+ confidence = node.receiver.name == :ObjectSpace ? :high : :none
31
+
31
32
 
32
- return unless confidence
33
33
 
34
34
  add_offense node, confidence: confidence
35
35
  end
@@ -10,9 +10,10 @@ module Deprecool
10
10
  removed_in '4.1.0'
11
11
  title 'Passing arguments to #to_set is deprecated'
12
12
  summary 'Since Ruby 4.0, passing arguments to Set#to_set / Enumerable#to_set ' \
13
- 'is deprecated and will be removed.'
14
- suggestion 'Call #to_set with no arguments. If you were building a ' \
15
- 'Set subclass, construct it explicitly instead.'
13
+ 'is deprecated and will be removed because it allowed for results that didn\'t make sense. ' \
14
+ 'For example: `enumerable.to_set(StandardError)`'
15
+ suggestion 'Call #to_set with no arguments. If you want an instance of a subclass of Set ' \
16
+ 'from an enumerable construct it explicitly instead: MySetSubclass.new(enumerable)'
16
17
  reference 'https://bugs.ruby-lang.org/issues/21390 https://github.com/ruby/ruby/pull/13489'
17
18
  effort :low
18
19
 
@@ -21,7 +22,7 @@ module Deprecool
21
22
  return unless node.arguments
22
23
 
23
24
  confidence = confidence_from_receiver_node(node.receiver)
24
- return unless confidence
25
+
25
26
 
26
27
  add_offense(node, confidence:)
27
28
  end
@@ -35,12 +36,12 @@ module Deprecool
35
36
  # a plain `to_set(x)` call to the current scope's own
36
37
  # method. This is probably not Enumerable#to_set unless someone has
37
38
  # monkeypatched Enumerable and is using to_set with an argument, I guess
38
- when nil then nil
39
+ when nil then :none
39
40
  when Prism::ConstantReadNode, Prism::ConstantPathNode
40
41
  # These nodes can constants like:
41
42
  # MY_DATA.to_set(arg) => likely an array, but can't be sure so :low
42
43
  # MyClass.to_set(arg) => has lowercase letters so it's a class method
43
- receiver&.name&.match?(/[a-z]/) ? nil : :low
44
+ receiver&.name&.match?(/[a-z]/) ? :none : :low
44
45
  else
45
46
  # this branch is when the receiver is some
46
47
  # local var, instance variable, method chain, safe navigation, etc.
@@ -0,0 +1,26 @@
1
+ # lib/gem_versions.rb
2
+
3
+ module Deprecool
4
+ class GemVersions
5
+ include Enumerable
6
+
7
+ attr_reader :gem_versions
8
+
9
+ def initialize
10
+ @gem_versions = []
11
+ end
12
+
13
+ def add(gem:, version:)
14
+ @gem_versions << { gem: gem.to_sym,
15
+ version: Gem::Version.new(version.to_s) }
16
+ end
17
+
18
+ def add_all!(gemversions)
19
+ Array(gemversions).each { |gv| add(gem: gv[:gem], version: gv[:version]) }
20
+ end
21
+
22
+ def each(&block)
23
+ @gem_versions.each(&block)
24
+ end
25
+ end
26
+ end
@@ -18,6 +18,12 @@ module PrismHelpers
18
18
  node.arguments
19
19
  end
20
20
 
21
+ def positional_args_from(arguments)
22
+ unwrap_arguments(arguments).to_a.reject do |arg|
23
+ arg.is_a?(Prism::KeywordHashNode) || arg.is_a?(Prism::BlockArgumentNode)
24
+ end
25
+ end
26
+
21
27
  # https://docs.ruby-lang.org/en/master/Prism/ArrayNode.html
22
28
  def unwrap_array(node)
23
29
  return node unless node.is_a?(Prism::ArrayNode)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsHelpers
4
+ def active_record_query_method_names
5
+ # this is as of 8.1.3.1
6
+ # https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html
7
+ %i[
8
+ and annotate create_with distinct
9
+ eager_load excluding extending extract_associated
10
+ from group having in_order_of includes invert_where
11
+ joins left_joins left_outer_joins limit lock
12
+ none offset optimizer_hints or order preload
13
+ readonly references regroup reorder reselect reverse_order rewhere
14
+ select strict_loading structurally_compatible?
15
+ uniq! unscope
16
+ where with with_recursive without
17
+ ]
18
+ end
19
+ end
@@ -7,7 +7,7 @@ module Deprecool
7
7
 
8
8
  # All the subclasses of Finder
9
9
  def all_finders
10
- Finder.registry
10
+ @all ||= Finder.subclasses
11
11
  end
12
12
 
13
13
  # Gets finders that apply to the given versions.
@@ -22,7 +22,7 @@ module Deprecool
22
22
  def finders_by_gem(gems)
23
23
  targets = []
24
24
  gems.each do |gem|
25
- targets << all_finders.select { it.to_s.match(/#{gem.capitalize}/) }
25
+ targets << all_finders.select { |finder| finder.to_s.match(/#{gem.capitalize}/) }
26
26
  end
27
27
  targets.flatten
28
28
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Deprecool
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.4'
5
5
  end
6
6
 
data/lib/deprecool.rb CHANGED
@@ -2,20 +2,23 @@
2
2
 
3
3
  require 'prism'
4
4
 
5
- module Deprecool
6
- class Error < StandardError; end
5
+ module Deprecool; end
6
+
7
+ Dir["#{__dir__}/deprecool/helpers/*.rb"].each do |helper|
8
+ require helper
7
9
  end
8
10
 
11
+ require_relative 'deprecool/errors'
9
12
  require_relative 'deprecool/version'
13
+ require_relative 'deprecool/gem_versions'
10
14
  require_relative 'deprecool/finder'
11
15
  require_relative 'deprecool/registry'
12
16
  require_relative 'deprecool/scanner'
13
- require_relative 'deprecool/lockfile_parser'
17
+ require_relative 'deprecool/dependency_parser'
14
18
  require_relative 'deprecool/cli'
15
19
 
16
20
  # TODO: Scan gemfiles and then only require relevant finders instead of all
17
21
  # of them
18
- Dir[File.join(__dir__, 'deprecool', 'finders', '**', '*.rb')].each do |finder|
22
+ Dir["#{__dir__}/deprecool/finders/**/*.rb"].each do |finder|
19
23
  require finder
20
24
  end
21
-
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deprecool
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zac Radford
@@ -54,15 +54,24 @@ files:
54
54
  - exe/deprecool
55
55
  - lib/deprecool.rb
56
56
  - lib/deprecool/cli.rb
57
+ - lib/deprecool/dependency_parser.rb
58
+ - lib/deprecool/dependency_parsers/gemspec_parser.rb
59
+ - lib/deprecool/dependency_parsers/lockfile_parser.rb
60
+ - lib/deprecool/dependency_parsers/null_parser.rb
61
+ - lib/deprecool/errors.rb
57
62
  - lib/deprecool/finder.rb
58
63
  - lib/deprecool/finders/rails/v7.1.0/serializer_positional_class_argument.rb
64
+ - lib/deprecool/finders/rails/v8.2.0/active_record_relation_uniq.rb
65
+ - lib/deprecool/finders/rails/v8.2.0/no_binds_to_insert_update_delete.rb
59
66
  - lib/deprecool/finders/rails/v8.2.0/redis_cache_store_default_redis_options.rb
60
67
  - lib/deprecool/finders/rails/v8.2.0/to_sql_binds.rb
61
68
  - lib/deprecool/finders/rails/v8.2.0/write_attribute_id.rb
69
+ - lib/deprecool/finders/ruby/v4.0.0/json_dump_limit.rb
62
70
  - lib/deprecool/finders/ruby/v4.0.0/object_space_id2ref.rb
63
71
  - lib/deprecool/finders/ruby/v4.0.0/to_set_arguments.rb
72
+ - lib/deprecool/gem_versions.rb
64
73
  - lib/deprecool/helpers/prism_helpers.rb
65
- - lib/deprecool/lockfile_parser.rb
74
+ - lib/deprecool/helpers/rails_helpers.rb
66
75
  - lib/deprecool/registry.rb
67
76
  - lib/deprecool/scanner.rb
68
77
  - lib/deprecool/version.rb
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler'
4
-
5
- module Deprecool
6
- module LockfileParser
7
- extend self
8
-
9
- def parse!(path_to_gemfile_lock = 'Gemfile.lock', parser: Bundler::LockfileParser)
10
- lockfile = File.read(path_to_gemfile_lock)
11
-
12
- parsed = parser.new(lockfile)
13
-
14
- versions = parsed.specs.map do |spec|
15
- { gem: spec.name, version: spec.version.to_s }
16
- end
17
-
18
- gem, version = parsed.ruby_version.split
19
- versions << { gem:, version: }
20
- end
21
- end
22
- end