deprecool 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +19 -0
- data/README.md +35 -0
- data/exe/deprecool +9 -0
- data/lib/deprecool/cli.rb +164 -0
- data/lib/deprecool/finder.rb +162 -0
- data/lib/deprecool/finders/rails/v7.1.0/serializer_positional_class_argument.rb +38 -0
- data/lib/deprecool/finders/rails/v8.2.0/redis_cache_store_default_redis_options.rb +34 -0
- data/lib/deprecool/finders/rails/v8.2.0/to_sql_binds.rb +39 -0
- data/lib/deprecool/finders/ruby/v4.0.0/object_space_id2ref.rb +40 -0
- data/lib/deprecool/finders/ruby/v4.0.0/to_set_arguments.rb +60 -0
- data/lib/deprecool/helpers/prism_helpers.rb +39 -0
- data/lib/deprecool/lockfile_parser.rb +22 -0
- data/lib/deprecool/registry.rb +58 -0
- data/lib/deprecool/scanner.rb +75 -0
- data/lib/deprecool/version.rb +6 -0
- data/lib/deprecool.rb +21 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8979f5deb74f90a5e9e6061319b79a1d920c6153bf3a32d378293514c6f5c547
|
|
4
|
+
data.tar.gz: 7a17ed36ecfb88aab21b1032a1fa74ae2165e8126826bae9cff2a341c5ae47a7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d61efb1f6c1a4b36570040aa91dbf20b856fa63b8564390732e3ef14de6ee704f899d6aace163c338db6bec7511f2d189e158d5819f80b75a9002709a1214c4c
|
|
7
|
+
data.tar.gz: 62ada454736be50a5ff349562d2500d8266ecbe23829cd5a078446abd768fb7677e21e676201da08c4b6f9e1e6329afa0a5a3e3c4b2891c0355777de10166bd4
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Zac Radford
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Deprecool
|
|
2
|
+
|
|
3
|
+
A static ruby code analyzer to find your deprecated code!
|
|
4
|
+
|
|
5
|
+
For any developer who has ever ignored a gem's warning and then ran into an error when they went to update
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Add to your gemfile: `gem 'deprecool'`
|
|
10
|
+
Or directly `gem install deprecool`
|
|
11
|
+
|
|
12
|
+
Then you can run `deprecool` in the command line to get:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Commands:
|
|
16
|
+
deprecool list # Display which finders are used in this version
|
|
17
|
+
deprecool scan [PATHS] # Scan files or directories for known deprecations
|
|
18
|
+
deprecool version # Print the deprecool version
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Scan
|
|
22
|
+
|
|
23
|
+
`deprecool scan` has several options that can be passed, or it can be run in a folder with a `Gemfile.lock` to automatically look for deprecations based on the currently installed gems.
|
|
24
|
+
|
|
25
|
+
- `--gem` takes a comma separated list of gems to scan for:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
deprecool scan --gem=rails,ruby
|
|
29
|
+
```
|
|
30
|
+
_(make sure there are no spaces between the gem names)_
|
|
31
|
+
|
|
32
|
+
- `--format=json` if you want the output as json instead of the default text
|
|
33
|
+
- `--lockfile` takes a path to your `Gemfile.lock`, it defaults to using the current directory
|
|
34
|
+
- `--all` run every finder regardless of applicability
|
|
35
|
+
- `--paths` an array of the files or directories to look for ruby file in, defaults to `'.'`
|
data/exe/deprecool
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'dry/cli'
|
|
5
|
+
require 'debug'
|
|
6
|
+
|
|
7
|
+
module Deprecool
|
|
8
|
+
# Command-line entry point. Scans the given files/directories and reports
|
|
9
|
+
# deprecations.
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
extend Dry::CLI::Registry
|
|
13
|
+
|
|
14
|
+
class Version < Dry::CLI::Command
|
|
15
|
+
desc 'Print the deprecool version'
|
|
16
|
+
|
|
17
|
+
def call(*)
|
|
18
|
+
puts "deprecool #{Deprecool::VERSION}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class List < Dry::CLI::Command
|
|
23
|
+
desc 'Display which finders are used in this version'
|
|
24
|
+
|
|
25
|
+
option :gems, type: :array, desc: 'Specify gem name(s) to list which versions have deprecation tracking available'
|
|
26
|
+
|
|
27
|
+
def call(gems:)
|
|
28
|
+
CLI.list_finders(Finder.registry)
|
|
29
|
+
|
|
30
|
+
# TODO: REPL to filter through the available finders?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Scan < Dry::CLI::Command
|
|
35
|
+
desc 'Scan files or directories for known deprecations'
|
|
36
|
+
|
|
37
|
+
argument :paths, type: :array, desc: 'Files or directories to scan'
|
|
38
|
+
option :lockfile, default: 'Gemfile.lock', desc: 'Path to Gemfile.lock, defaults to current directory'
|
|
39
|
+
option :gems, type: :array, desc: 'Gems to scan for: --gems=ruby,rails'
|
|
40
|
+
option :format, default: 'text', values: %w[text json], desc: 'Output format'
|
|
41
|
+
option :all, type: :boolean, default: false, desc: 'Run every finder regardless of version'
|
|
42
|
+
|
|
43
|
+
def call(paths:, format:, gems: [], all:, lockfile:, **)
|
|
44
|
+
json_output = format == 'json'
|
|
45
|
+
files = CLI.ruby_files(paths)
|
|
46
|
+
|
|
47
|
+
if files.empty?
|
|
48
|
+
if paths.empty?
|
|
49
|
+
warn CLI.colorize('deprecool: please supply a path/to/file/or/directory', :red)
|
|
50
|
+
exit 2
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
warn CLI.colorize("deprecool: no Ruby files found in #{paths.join(', ')}", :red)
|
|
54
|
+
exit 2
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if gems.any?
|
|
58
|
+
puts "Scanning for deprecations from: #{gems.join(', ')}" unless json_output
|
|
59
|
+
# we won't be scanning a lockfile
|
|
60
|
+
gem_versions = []
|
|
61
|
+
elsif all
|
|
62
|
+
gem_versions = []
|
|
63
|
+
gems = []
|
|
64
|
+
else
|
|
65
|
+
puts "Scanning #{lockfile}..." unless json_output
|
|
66
|
+
gem_versions = LockfileParser.parse!(lockfile)
|
|
67
|
+
|
|
68
|
+
# we don't need to look for individual gems when scanning gemfiles
|
|
69
|
+
gems = []
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
finders = Registry.applicable(gems:, gem_versions:, include_all: all)
|
|
73
|
+
scanner = Scanner.new(finders)
|
|
74
|
+
offenses = files.flat_map { |file| scanner.scan_file(file) }
|
|
75
|
+
.sort_by { |offense| [offense.file_path, offense.line, offense.column] }
|
|
76
|
+
|
|
77
|
+
offenses = offenses.group_by(&:id) unless json_output
|
|
78
|
+
|
|
79
|
+
CLI.report(offenses, format, finders)
|
|
80
|
+
exit(offenses.empty? ? 0 : 1)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
register 'version', Version, aliases: %w[v -v --version]
|
|
85
|
+
register 'scan', Scan
|
|
86
|
+
register 'list', List, aliases: %w[l -l --list]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class << self
|
|
90
|
+
def start(argv)
|
|
91
|
+
Dry::CLI.new(Commands).call(arguments: argv)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def ruby_files(paths)
|
|
95
|
+
paths = paths.empty? ? ['.'] : paths
|
|
96
|
+
|
|
97
|
+
paths.flat_map do |path|
|
|
98
|
+
if File.directory?(path)
|
|
99
|
+
Dir[File.join(path, '**', '*.rb')]
|
|
100
|
+
elsif File.file?(path)
|
|
101
|
+
[path]
|
|
102
|
+
else
|
|
103
|
+
warn "deprecool: no such file or directory: #{path}"
|
|
104
|
+
[]
|
|
105
|
+
end
|
|
106
|
+
end.uniq.sort
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def report(offenses, format, finders)
|
|
110
|
+
if format == 'json'
|
|
111
|
+
puts JSON.pretty_generate(offenses.map(&:to_h))
|
|
112
|
+
else
|
|
113
|
+
text_report(offenses, finders)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def text_report(offenses, finders)
|
|
118
|
+
if offenses.empty?
|
|
119
|
+
puts colorize('No deprecations found.', :green)
|
|
120
|
+
puts "(#{finders.size} finder#{'s' unless finders.size == 1} active)"
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
offense_count = 0
|
|
125
|
+
|
|
126
|
+
offenses.each_value do |offense_array|
|
|
127
|
+
offense = offense_array.first
|
|
128
|
+
|
|
129
|
+
puts "\n#{colorize(offense.title, :bold)}\n"
|
|
130
|
+
puts " * #{offense.summary}"
|
|
131
|
+
puts " #{colorize('fix:', :green)} #{offense.suggestion}" if offense.suggestion
|
|
132
|
+
puts " #{colorize('source:', :green)} #{offense.reference}" if offense.reference
|
|
133
|
+
puts ' Found At:'
|
|
134
|
+
offense_array.each do |o|
|
|
135
|
+
offense_count += 1
|
|
136
|
+
confidence = o.confidence
|
|
137
|
+
confidence_color = confidence == :high ? :red : :yellow
|
|
138
|
+
badge = colorize("(#{confidence} confidence)", confidence_color)
|
|
139
|
+
|
|
140
|
+
puts " (#{offense_count}) #{colorize(o.location, :cyan)} #{badge}"
|
|
141
|
+
puts " #{o.source_line.strip}" if o.source_line
|
|
142
|
+
puts
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
puts colorize("#{offense_count} deprecation#{'s' unless offense_count == 1} found.", :red)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def list_finders(finders)
|
|
150
|
+
puts 'Active finders:'
|
|
151
|
+
finders.sort_by { [it.gem.to_s, it.deprecated_in.to_s, it.id.to_s] }.each do
|
|
152
|
+
puts " - #{it.classname.ljust(35)} (#{it.gem} #{it.deprecated_in.to_s}) — #{it.title}"
|
|
153
|
+
end
|
|
154
|
+
puts ' (none)' if finders.empty?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def colorize(text, color)
|
|
158
|
+
colors = { red: 31, green: 32, yellow: 33, cyan: 36, bold: 1 }
|
|
159
|
+
|
|
160
|
+
"\e[#{colors.fetch(color, nil)}m#{text}\e[0m"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# requiring this here so we don't have to remember to add
|
|
4
|
+
# it in individual finders
|
|
5
|
+
require_relative 'helpers/prism_helpers'
|
|
6
|
+
|
|
7
|
+
module Deprecool
|
|
8
|
+
# Base class for every deprecation finder.
|
|
9
|
+
#
|
|
10
|
+
# A finder is responsible for detecting a single deprecation.
|
|
11
|
+
# Subclasses of Finder describe the deprecation with the class-level DSL and
|
|
12
|
+
# then implement one or more Prism visit hooks (e.g. `on_call_node`).
|
|
13
|
+
#
|
|
14
|
+
# A Scanner then follows the AST a single time and dispatches each node to the
|
|
15
|
+
# finders that have a relevant method, which call `add_offense` when a match is found.
|
|
16
|
+
#
|
|
17
|
+
# Example child class
|
|
18
|
+
#
|
|
19
|
+
# class MyFinder < Deprecool::Finder
|
|
20
|
+
# gem :ruby
|
|
21
|
+
# deprecated_in '4.0.0'
|
|
22
|
+
# removed_in '4.1.0'
|
|
23
|
+
# title 'some_method will be removed'
|
|
24
|
+
# summary 'some_method was causing a problem and will be removed soon'
|
|
25
|
+
# suggestion 'remove some_method'
|
|
26
|
+
# reference 'https://link_to_pr_or_relevant_info'
|
|
27
|
+
# effort :medium
|
|
28
|
+
#
|
|
29
|
+
# def on_call_node(node)
|
|
30
|
+
# add_offense(node, confidence: :high) if node.name == :some_method
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
class Finder
|
|
34
|
+
include PrismHelpers
|
|
35
|
+
|
|
36
|
+
@registry = []
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# All finder subclasses, in definition order.
|
|
40
|
+
attr_reader :registry
|
|
41
|
+
|
|
42
|
+
def inherited(subclass)
|
|
43
|
+
Finder.registry << subclass
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# defines class methods that set instance variables
|
|
48
|
+
%i[gem title summary suggestion reference].each do |attribute|
|
|
49
|
+
define_method(attribute) do |value = (getter = true)|
|
|
50
|
+
return instance_variable_get("@#{attribute}") if getter
|
|
51
|
+
|
|
52
|
+
instance_variable_set("@#{attribute}", value)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Stored as a Gem::Version so it can be compared against the app's
|
|
57
|
+
# detected version.
|
|
58
|
+
%i[deprecated_in removed_in].each do |attribute|
|
|
59
|
+
define_method(attribute) do |value = (getter = true)|
|
|
60
|
+
return instance_variable_get("@#{attribute}") if getter
|
|
61
|
+
|
|
62
|
+
instance_variable_set("@#{attribute}", Gem::Version.new(value))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# how much work is this to fix?
|
|
67
|
+
# low => Rails::v7_1_0::SerializerPositionalClassArgument just changes a method signature to have kwarg
|
|
68
|
+
# medium => Ruby::v4_0_0::ObjectSpaceId2ref to keep same functionality you need to remove the method
|
|
69
|
+
# and minor refactor to use WeakMap or something
|
|
70
|
+
# high => you're gonna need to make some changes to preserve the same functionality
|
|
71
|
+
def effort(value = (getter = true))
|
|
72
|
+
return @effort if getter
|
|
73
|
+
|
|
74
|
+
values = %i[low medium high]
|
|
75
|
+
|
|
76
|
+
raise "Please use a standardized effort value, i.e #{values}" unless values.include?(value)
|
|
77
|
+
|
|
78
|
+
@effort = value
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def affected_version_range
|
|
82
|
+
[deprecated_in, removed_in]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# return just the class name without all the modules, for displaying
|
|
86
|
+
def classname
|
|
87
|
+
name.split('::').last
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# this is used internally to sort Finders
|
|
91
|
+
# so we might as well not sort the part thats repeated for every finder
|
|
92
|
+
def id
|
|
93
|
+
name.delete_prefix('Deprecool::Finders::')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# This is what the Scanner class calls to see what methods are
|
|
97
|
+
# defined on the child classes,
|
|
98
|
+
#
|
|
99
|
+
# child classes should define the methods with 'on' in place of 'visit'
|
|
100
|
+
# so that we can differentiate them from the default implementation
|
|
101
|
+
# provided by Prism::Visitor
|
|
102
|
+
#
|
|
103
|
+
# (see https://docs.ruby-lang.org/en/master/Prism/Visitor.html for the full list of
|
|
104
|
+
# Prism compatible methods)
|
|
105
|
+
# some examples of prism compatible 'on_node' methods for a finder:
|
|
106
|
+
# Prism::VisitClassNode => on_class_node
|
|
107
|
+
# Prism::VisitDefNode => on_def_node
|
|
108
|
+
# Prism::VisitModuleNode => on_module_node
|
|
109
|
+
# Prism::
|
|
110
|
+
def hook_methods
|
|
111
|
+
instance_methods(false).grep(/\Aon_\w+_node\z/)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
attr_reader :file_path, :source
|
|
116
|
+
|
|
117
|
+
def initialize(file_path, source, offenses)
|
|
118
|
+
@file_path = file_path
|
|
119
|
+
@source = source # the result of Prism.parse
|
|
120
|
+
@offenses = offenses
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
Offense = Struct.new(:file_path, :line, :column, :end_line, :end_column,
|
|
124
|
+
:id, :title, :summary, :suggestion, :reference,
|
|
125
|
+
:effort, :confidence, :gem, :deprecated_in,
|
|
126
|
+
:removed_in, :snippet, :source_line) do
|
|
127
|
+
def location
|
|
128
|
+
"#{file_path}:#{line}:#{column + 1}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Record a deprecation at the given node's location.
|
|
135
|
+
#
|
|
136
|
+
# confidence - [:high, :low], but currently the only check for this is
|
|
137
|
+
# from the CLI outputting red or yellow depending on confidence
|
|
138
|
+
def add_offense(node, confidence: :high)
|
|
139
|
+
location = node.location # a Prism::Location
|
|
140
|
+
|
|
141
|
+
@offenses << Offense.new(
|
|
142
|
+
file_path: file_path,
|
|
143
|
+
line: location.start_line,
|
|
144
|
+
column: location.start_column,
|
|
145
|
+
end_line: location.end_line,
|
|
146
|
+
end_column: location.end_column,
|
|
147
|
+
id: self.class.id,
|
|
148
|
+
title: self.class.title,
|
|
149
|
+
summary: self.class.summary,
|
|
150
|
+
suggestion: self.class.suggestion,
|
|
151
|
+
reference: self.class.reference,
|
|
152
|
+
effort: self.class.effort,
|
|
153
|
+
confidence: confidence,
|
|
154
|
+
gem: self.class.gem,
|
|
155
|
+
deprecated_in: self.class.deprecated_in.to_s,
|
|
156
|
+
removed_in: self.class.removed_in.to_s,
|
|
157
|
+
snippet: location.slice,
|
|
158
|
+
source_line: source.lines[location.start_line - 1]&.chomp
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deprecool
|
|
4
|
+
module Finders
|
|
5
|
+
module Rails
|
|
6
|
+
module V7_1_0
|
|
7
|
+
class SerializerPositionalClassArgument < Deprecool::Finder
|
|
8
|
+
gem :rails
|
|
9
|
+
deprecated_in '7.1.0'
|
|
10
|
+
removed_in '7.2.0'
|
|
11
|
+
title '"serialize" method singature change'
|
|
12
|
+
summary 'The serialize method in active_record no longer accepts a class as a positional argument ' \
|
|
13
|
+
'the method signature now requires custom serializer classes to be ' \
|
|
14
|
+
'passed with the `coder:` keword argument.'
|
|
15
|
+
suggestion 'add the keyword `coder:` to the method signature, i.e serialize :attr, coder: CustomJsonEncoder'
|
|
16
|
+
reference 'https://github.com/rails/rails/pull/47463'
|
|
17
|
+
effort :low
|
|
18
|
+
|
|
19
|
+
# TODO: add check for the `serialize` method being called by a Rails model?,
|
|
20
|
+
# like add a on_constant_path_node for ActiveRecord::Base
|
|
21
|
+
# or a class_node with a superclass of ActiveRecord::Base
|
|
22
|
+
# def on_class_node node
|
|
23
|
+
# node
|
|
24
|
+
# end
|
|
25
|
+
|
|
26
|
+
def on_call_node(node)
|
|
27
|
+
return unless node.name == :serialize
|
|
28
|
+
|
|
29
|
+
arguments_array = unwrap_arguments(node.arguments)
|
|
30
|
+
return unless arguments_array && arguments_array[1]
|
|
31
|
+
|
|
32
|
+
add_offense(node, confidence: :high) if arguments_array[1].is_a?(Prism::ConstantReadNode)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deprecool
|
|
4
|
+
module Finders
|
|
5
|
+
module Rails
|
|
6
|
+
module V8_2_0
|
|
7
|
+
class RedisCacheStoreDefaultRedisOptions < Deprecool::Finder
|
|
8
|
+
gem :rails
|
|
9
|
+
deprecated_in '8.2.0'
|
|
10
|
+
removed_in '9.0.0'
|
|
11
|
+
title 'RedisCacheStore::DEFAULT_REDIS_OPTIONS is deprecated'
|
|
12
|
+
summary 'The `redis-client` implementation no longer reads this constant.'
|
|
13
|
+
suggestion 'Pass timeout options to RedisCacheStore or a configured RedisClient instead.'
|
|
14
|
+
reference 'https://github.com/rails/rails/pull/58191 '
|
|
15
|
+
effort :low
|
|
16
|
+
|
|
17
|
+
# constant_read is any constant, like `Foo`
|
|
18
|
+
def on_constant_read_node(node)
|
|
19
|
+
return unless node.name == :DEFAULT_REDIS_OPTIONS
|
|
20
|
+
|
|
21
|
+
add_offense(node, confidence: :high)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# a constant_path is like: ActiveSupport::Cache::RedisCacheStore::DEFAULT_REDIS_OPTIONS
|
|
25
|
+
def on_constant_path_node(node)
|
|
26
|
+
return unless node.name == :DEFAULT_REDIS_OPTIONS
|
|
27
|
+
|
|
28
|
+
add_offense(node, confidence: :high)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deprecool
|
|
4
|
+
module Finders
|
|
5
|
+
module Rails
|
|
6
|
+
module V8_2_0
|
|
7
|
+
class ToSqlBinds < Deprecool::Finder
|
|
8
|
+
gem :rails
|
|
9
|
+
deprecated_in '8.2.0'
|
|
10
|
+
removed_in '8.3.0'
|
|
11
|
+
title 'Passing "binds" into "to_sql" is deprecated'
|
|
12
|
+
summary 'Since Rails 5.2, bind parameters live on the Arel AST ' \
|
|
13
|
+
'to_sql_and_binds no longer uses binds for SQL construction, ' \
|
|
14
|
+
'and to_sql discards the output binds either way'
|
|
15
|
+
suggestion 'Do not use the binds argument of the to_sql method'
|
|
16
|
+
reference 'https://github.com/rails/rails/pull/58310'
|
|
17
|
+
effort :low
|
|
18
|
+
|
|
19
|
+
def on_call_node(node)
|
|
20
|
+
# to_sql is a method on an ActiveRecord::Base.connection
|
|
21
|
+
# the first argument should be a string of sql
|
|
22
|
+
# the second argument used to be an array of binds
|
|
23
|
+
# == the second argument is now deprecated ==
|
|
24
|
+
#
|
|
25
|
+
# the name of the connection is likely to be connection, but that's unreliable
|
|
26
|
+
# if a method called 'to_sql' is passed two arguments and the second is an
|
|
27
|
+
# array that's probably good enough
|
|
28
|
+
return unless node.name == :to_sql
|
|
29
|
+
|
|
30
|
+
arguments_array = unwrap_arguments(node.arguments)
|
|
31
|
+
return unless arguments_array && arguments_array[1]
|
|
32
|
+
|
|
33
|
+
add_offense(node, confidence: :high)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deprecool
|
|
4
|
+
module Finders
|
|
5
|
+
module Ruby
|
|
6
|
+
module V4_0_0
|
|
7
|
+
class ObjectSpaceId2ref < Deprecool::Finder
|
|
8
|
+
gem :ruby
|
|
9
|
+
deprecated_in '4.0.0'
|
|
10
|
+
removed_in '4.1.0'
|
|
11
|
+
title 'Using ObjectSpace._id2ref is deprecated'
|
|
12
|
+
summary 'The object_id identifier does not guarantee that the id won\'t be reused ' \
|
|
13
|
+
'after the original has been garbage collected, ' \
|
|
14
|
+
'therefore _id2ref is unsafe and unreliable, and per matz: ' \
|
|
15
|
+
'"Reviving arbitrary objects from integer IDs was never a sound API"'
|
|
16
|
+
suggestion 'Do not rely on this method'
|
|
17
|
+
reference 'original issue: https://bugs.ruby-lang.org/issues/15408' \
|
|
18
|
+
'deprecated: https://github.com/ruby/ruby/pull/13157' \
|
|
19
|
+
'removed: https://bugs.ruby-lang.org/issues/22135'
|
|
20
|
+
effort :medium
|
|
21
|
+
|
|
22
|
+
def on_call_node(node)
|
|
23
|
+
return unless node.name == :_id2ref
|
|
24
|
+
|
|
25
|
+
# The method name '_id2ref' is quite unique, and
|
|
26
|
+
# the use case for this is specific enough that
|
|
27
|
+
# I don't think we need much more than this
|
|
28
|
+
# I'm open to be wrong though, maybe lots of people are subclassing
|
|
29
|
+
# ObjectSpace or defining '_id2ref' on custom classes
|
|
30
|
+
confidence = node.receiver.name == :ObjectSpace ? :high : nil
|
|
31
|
+
|
|
32
|
+
return unless confidence
|
|
33
|
+
|
|
34
|
+
add_offense node, confidence: confidence
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deprecool
|
|
4
|
+
module Finders
|
|
5
|
+
module Ruby
|
|
6
|
+
module V4_0_0
|
|
7
|
+
class ToSetArguments < Deprecool::Finder
|
|
8
|
+
gem :ruby
|
|
9
|
+
deprecated_in '4.0.0'
|
|
10
|
+
removed_in '4.1.0'
|
|
11
|
+
title 'Passing arguments to #to_set is deprecated'
|
|
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.'
|
|
16
|
+
reference 'https://bugs.ruby-lang.org/issues/21390 https://github.com/ruby/ruby/pull/13489'
|
|
17
|
+
effort :low
|
|
18
|
+
|
|
19
|
+
def on_call_node(node)
|
|
20
|
+
return unless node.name == :to_set
|
|
21
|
+
return unless node.arguments
|
|
22
|
+
|
|
23
|
+
confidence = confidence_from_receiver_node(node.receiver)
|
|
24
|
+
return unless confidence
|
|
25
|
+
|
|
26
|
+
add_offense(node, confidence:)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def confidence_from_receiver_node(receiver)
|
|
32
|
+
case receiver
|
|
33
|
+
# these nodes all represent Enumerables
|
|
34
|
+
when Prism::ArrayNode, Prism::HashNode, Prism::RangeNode then :high
|
|
35
|
+
# a plain `to_set(x)` call to the current scope's own
|
|
36
|
+
# method. This is probably not Enumerable#to_set unless someone has
|
|
37
|
+
# monkeypatched Enumerable and is using to_set with an argument, I guess
|
|
38
|
+
when nil then nil
|
|
39
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
40
|
+
# These nodes can constants like:
|
|
41
|
+
# MY_DATA.to_set(arg) => likely an array, but can't be sure so :low
|
|
42
|
+
# MyClass.to_set(arg) => has lowercase letters so it's a class method
|
|
43
|
+
receiver&.name&.match?(/[a-z]/) ? nil : :low
|
|
44
|
+
else
|
|
45
|
+
# this branch is when the receiver is some
|
|
46
|
+
# local var, instance variable, method chain, safe navigation, etc.
|
|
47
|
+
# could even be an a custom object — we can't really tell.
|
|
48
|
+
# but we do know enumerable methods like map, reduce, etc. so
|
|
49
|
+
# we can be more confident about that
|
|
50
|
+
return :high if Enumerable.method_defined?(receiver.name)
|
|
51
|
+
|
|
52
|
+
:low
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrismHelpers
|
|
4
|
+
# `(1..10)` parses as a ParenthesesNode wrapping the real expression.
|
|
5
|
+
def unwrap_parentheses(node)
|
|
6
|
+
return node unless node.is_a?(Prism::ParenthesesNode)
|
|
7
|
+
|
|
8
|
+
body = node.body&.body
|
|
9
|
+
body&.length == 1 ? body.first : node
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Method arguments are represented as:
|
|
13
|
+
# node.arguments returns a literal array of arguments
|
|
14
|
+
# https://docs.ruby-lang.org/en/master/Prism/ArgumentsNode.html
|
|
15
|
+
def unwrap_arguments(node)
|
|
16
|
+
return node unless node.is_a?(Prism::ArgumentsNode)
|
|
17
|
+
|
|
18
|
+
node.arguments
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# https://docs.ruby-lang.org/en/master/Prism/ArrayNode.html
|
|
22
|
+
def unwrap_array(node)
|
|
23
|
+
return node unless node.is_a?(Prism::ArrayNode)
|
|
24
|
+
|
|
25
|
+
node.elements
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def unwrap_class(node)
|
|
29
|
+
return node unless node.is_a?(Prism::ClassNode)
|
|
30
|
+
|
|
31
|
+
node.body
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def unwrap_children(node)
|
|
35
|
+
return node unless node.respond_to?(:child_nodes)
|
|
36
|
+
|
|
37
|
+
node.child_nodes
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deprecool
|
|
4
|
+
# Selects which finders should run for a given application, based on the gems
|
|
5
|
+
# (and Ruby) it actually uses and at what versions.
|
|
6
|
+
module Registry
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
# Every registered finder.
|
|
10
|
+
def all_finders
|
|
11
|
+
Finder.registry
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Gets finders that apply to the given versions.
|
|
15
|
+
# gems - an array of gem names
|
|
16
|
+
# gem_versions - an array of hashes like: { gem:, version: }
|
|
17
|
+
def applicable(gems: [], gem_versions: [], include_all: false)
|
|
18
|
+
return all_finders if include_all
|
|
19
|
+
return finders_by_gem(gems) if gems.any?
|
|
20
|
+
return finders_by_gem_version(gem_versions) if gem_versions.any?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def finders_by_gem(gems)
|
|
24
|
+
targets = []
|
|
25
|
+
gems.each do |gem|
|
|
26
|
+
targets << all_finders.select { it.to_s.match(/#{gem.capitalize}/) }
|
|
27
|
+
end
|
|
28
|
+
targets.flatten
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def finders_by_gem_version(gem_versions)
|
|
32
|
+
finder_targets = []
|
|
33
|
+
|
|
34
|
+
gem_versions.each do |gem_version|
|
|
35
|
+
jem = gem_version[:gem].to_sym
|
|
36
|
+
version = Gem::Version.new(gem_version[:version])
|
|
37
|
+
|
|
38
|
+
# match when:
|
|
39
|
+
# 1) finder is for the current gem,
|
|
40
|
+
# 2) gem's version is higher than finder's deprecated,
|
|
41
|
+
# and 3) gems version is lower or equal to the removed_in
|
|
42
|
+
targets = all_finders.select do |finder|
|
|
43
|
+
(finder.gem == jem) &&
|
|
44
|
+
(finder.deprecated_in <= version) && (version <= finder.removed_in)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if !targets.empty?
|
|
48
|
+
finder_targets << targets
|
|
49
|
+
else
|
|
50
|
+
# this unfortunately floods the terminal for now, but one day maybe it won't
|
|
51
|
+
# puts "Oops, I don't have a Finder that applies to #{jem} v#{version}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
finder_targets.flatten
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deprecool
|
|
4
|
+
# Parses a source file with Prism and runs a set of finders against it in a
|
|
5
|
+
# single AST traversal.
|
|
6
|
+
class Scanner
|
|
7
|
+
def initialize(finders)
|
|
8
|
+
@finders = Array(finders)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def scan_file(path)
|
|
12
|
+
scan_source(File.read(path), path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Returns an Array<Offense>. Files that fail to parse yield no offenses;
|
|
16
|
+
# parse errors are surfaced separately via {#parse_errors}.
|
|
17
|
+
def scan_source(source, path = '(source)')
|
|
18
|
+
result = Prism.parse(source)
|
|
19
|
+
return [] unless result.success?
|
|
20
|
+
|
|
21
|
+
offenses = []
|
|
22
|
+
# each finder has the path, the whole source of the file, and the offenses list
|
|
23
|
+
# so that they can build the offense warning themselves.
|
|
24
|
+
# we might be able to refactor this to only pass the source because the path
|
|
25
|
+
# and offenses are only needed for the add_offense method, not the actual
|
|
26
|
+
# searching for an offense?
|
|
27
|
+
instances = @finders.map { |finder| finder.new(path, source, offenses) }
|
|
28
|
+
DispatchVisitor.new(instances).visit(result.value)
|
|
29
|
+
offenses
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Combines the given Finder classes into one class by
|
|
33
|
+
# defining one method per Prism::Visitor hook (e.g. visit_call_node)
|
|
34
|
+
# based on all the given finders that define a method that matches that
|
|
35
|
+
# hook method name
|
|
36
|
+
class DispatchVisitor < Prism::Visitor
|
|
37
|
+
# finder_instances is the array passed to the Scanner.new class,
|
|
38
|
+
# so this would be [Ruby::V4_0_0::ToSetArguments, ..]
|
|
39
|
+
def initialize(finder_instances)
|
|
40
|
+
super()
|
|
41
|
+
|
|
42
|
+
dispatch = Hash.new { |hash, key| hash[key] = [] }
|
|
43
|
+
finder_instances.each do |instance|
|
|
44
|
+
# find the hooks and add the instances to the hash of hook methods
|
|
45
|
+
# so { on_call_node: [ToSetArguments.new, ObjectSpaceId2ref.new,.. ], ... }
|
|
46
|
+
instance.class.hook_methods.each { |hook| dispatch[hook] << instance }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
dispatch.each do |hook, finders|
|
|
50
|
+
# convert the instances' hooks to what prism::visitor expects
|
|
51
|
+
# so finder classes must use this pattern to name the visit_node methods
|
|
52
|
+
#
|
|
53
|
+
# 'on_call_node' => good
|
|
54
|
+
# 'find_call_node' => bad
|
|
55
|
+
visit_method = hook.to_s.sub(/\Aon_/, 'visit_').to_sym
|
|
56
|
+
|
|
57
|
+
# define the prism::visitor hook to loop through each of the
|
|
58
|
+
# related finders and call the name of the hook like so:
|
|
59
|
+
#
|
|
60
|
+
# def visit_call_node(node)
|
|
61
|
+
# [ToSetArguments.new, ObjectSpaceId2ref.new].each do |finder|
|
|
62
|
+
# finder.send("on_call_node", node)
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
# super(node)
|
|
66
|
+
# end
|
|
67
|
+
define_singleton_method(visit_method) do |node|
|
|
68
|
+
finders.each { |finder| finder.send(hook, node) }
|
|
69
|
+
super(node)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/deprecool.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
5
|
+
module Deprecool
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative 'deprecool/version'
|
|
10
|
+
require_relative 'deprecool/finder'
|
|
11
|
+
require_relative 'deprecool/registry'
|
|
12
|
+
require_relative 'deprecool/scanner'
|
|
13
|
+
require_relative 'deprecool/lockfile_parser'
|
|
14
|
+
require_relative 'deprecool/cli'
|
|
15
|
+
|
|
16
|
+
# TODO: Scan gemfiles and then only require relevant finders instead of all
|
|
17
|
+
# of them
|
|
18
|
+
Dir[File.join(__dir__, 'deprecool', 'finders', '**', '*.rb')].each do |finder|
|
|
19
|
+
require finder
|
|
20
|
+
end
|
|
21
|
+
|
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: deprecool
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Zac Radford
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: dry-cli
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: prism
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.0'
|
|
40
|
+
description: Static deprecation analysis tool to keep your code cool
|
|
41
|
+
email:
|
|
42
|
+
- zacnradford@gmail.com
|
|
43
|
+
executables:
|
|
44
|
+
- deprecool
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files:
|
|
47
|
+
- CHANGELOG.md
|
|
48
|
+
- LICENSE
|
|
49
|
+
- README.md
|
|
50
|
+
files:
|
|
51
|
+
- CHANGELOG.md
|
|
52
|
+
- LICENSE
|
|
53
|
+
- README.md
|
|
54
|
+
- exe/deprecool
|
|
55
|
+
- lib/deprecool.rb
|
|
56
|
+
- lib/deprecool/cli.rb
|
|
57
|
+
- lib/deprecool/finder.rb
|
|
58
|
+
- lib/deprecool/finders/rails/v7.1.0/serializer_positional_class_argument.rb
|
|
59
|
+
- lib/deprecool/finders/rails/v8.2.0/redis_cache_store_default_redis_options.rb
|
|
60
|
+
- lib/deprecool/finders/rails/v8.2.0/to_sql_binds.rb
|
|
61
|
+
- lib/deprecool/finders/ruby/v4.0.0/object_space_id2ref.rb
|
|
62
|
+
- lib/deprecool/finders/ruby/v4.0.0/to_set_arguments.rb
|
|
63
|
+
- lib/deprecool/helpers/prism_helpers.rb
|
|
64
|
+
- lib/deprecool/lockfile_parser.rb
|
|
65
|
+
- lib/deprecool/registry.rb
|
|
66
|
+
- lib/deprecool/scanner.rb
|
|
67
|
+
- lib/deprecool/version.rb
|
|
68
|
+
homepage: https://github.com/zradford/deprecool
|
|
69
|
+
licenses:
|
|
70
|
+
- MIT
|
|
71
|
+
metadata:
|
|
72
|
+
changelog_uri: https://github.com/zradford/deprecool/CHANGELOG.md
|
|
73
|
+
source_code_uri: https://github.com/zradford/deprecool
|
|
74
|
+
bug_tracker_uri: https://github.com/zradford/deprecool/issues
|
|
75
|
+
rubygems_mfa_required: 'true'
|
|
76
|
+
rdoc_options: []
|
|
77
|
+
require_paths:
|
|
78
|
+
- lib
|
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '3.3'
|
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
requirements: []
|
|
90
|
+
rubygems_version: 4.0.6
|
|
91
|
+
specification_version: 4
|
|
92
|
+
summary: Static deprecation analysis tool to keep your code cool
|
|
93
|
+
test_files: []
|