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 +4 -4
- data/CHANGELOG.md +15 -0
- data/lib/deprecool/cli.rb +87 -43
- data/lib/deprecool/dependency_parser.rb +32 -0
- data/lib/deprecool/dependency_parsers/gemspec_parser.rb +41 -0
- data/lib/deprecool/dependency_parsers/lockfile_parser.rb +35 -0
- data/lib/deprecool/dependency_parsers/null_parser.rb +15 -0
- data/lib/deprecool/errors.rb +23 -0
- data/lib/deprecool/finder.rb +9 -11
- data/lib/deprecool/finders/rails/v8.2.0/active_record_relation_uniq.rb +53 -0
- data/lib/deprecool/finders/rails/v8.2.0/no_binds_to_insert_update_delete.rb +55 -0
- data/lib/deprecool/finders/ruby/v4.0.0/json_dump_limit.rb +68 -0
- data/lib/deprecool/finders/ruby/v4.0.0/object_space_id2ref.rb +2 -2
- data/lib/deprecool/finders/ruby/v4.0.0/to_set_arguments.rb +7 -6
- data/lib/deprecool/gem_versions.rb +26 -0
- data/lib/deprecool/helpers/prism_helpers.rb +6 -0
- data/lib/deprecool/helpers/rails_helpers.rb +19 -0
- data/lib/deprecool/registry.rb +2 -2
- data/lib/deprecool/version.rb +1 -1
- data/lib/deprecool.rb +8 -5
- metadata +11 -2
- data/lib/deprecool/lockfile_parser.rb +0 -22
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 354619cb58b3d0b43f1aaffbd90720589a350d8d31f9f52a7c8cab5db4ee8125
|
|
4
|
+
data.tar.gz: b9ae44fa91e1c5c7815902810596f82c115981f35982ac3c1bedc89b78f8d7ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
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
|
|
58
|
+
files = CLI.ruby_files(paths)
|
|
45
59
|
|
|
46
|
-
if
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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)}
|
|
158
|
+
puts "\n#{offense.gem} #{offense.deprecated_in} - #{colorize(offense.title, :bold)}"
|
|
159
|
+
puts
|
|
129
160
|
puts " * #{offense.summary}"
|
|
130
|
-
puts
|
|
131
|
-
puts " #{colorize('
|
|
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 { [
|
|
151
|
-
puts " - #{
|
|
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
|
data/lib/deprecool/finder.rb
CHANGED
|
@@ -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
|
-
#
|
|
38
|
-
attr_reader :registry
|
|
39
|
-
|
|
33
|
+
# automatically include GemHelpers into the classes for the Gem
|
|
40
34
|
def inherited(subclass)
|
|
41
|
-
|
|
42
|
-
|
|
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],
|
|
134
|
-
#
|
|
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 :
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
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]/) ?
|
|
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
|
data/lib/deprecool/registry.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Deprecool
|
|
|
7
7
|
|
|
8
8
|
# All the subclasses of Finder
|
|
9
9
|
def all_finders
|
|
10
|
-
Finder.
|
|
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 {
|
|
25
|
+
targets << all_finders.select { |finder| finder.to_s.match(/#{gem.capitalize}/) }
|
|
26
26
|
end
|
|
27
27
|
targets.flatten
|
|
28
28
|
end
|
data/lib/deprecool/version.rb
CHANGED
data/lib/deprecool.rb
CHANGED
|
@@ -2,20 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
require 'prism'
|
|
4
4
|
|
|
5
|
-
module Deprecool
|
|
6
|
-
|
|
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/
|
|
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[
|
|
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.
|
|
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/
|
|
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
|