rubocop-dynamic_methods 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 +21 -0
- data/LICENSE +21 -0
- data/README.ja.md +147 -0
- data/README.md +124 -0
- data/config/default.yml +24 -0
- data/lib/rubocop/cop/dynamic_methods/define_method.rb +86 -0
- data/lib/rubocop/cop/dynamic_methods/delegation.rb +99 -0
- data/lib/rubocop/cop/dynamic_methods/method_missing.rb +29 -0
- data/lib/rubocop/cop/dynamic_methods/reporting.rb +57 -0
- data/lib/rubocop/cop/dynamic_methods/string_eval.rb +110 -0
- data/lib/rubocop/dynamic_methods/plugin.rb +32 -0
- data/lib/rubocop/dynamic_methods/rewrite.rb +822 -0
- data/lib/rubocop/dynamic_methods/version.rb +7 -0
- data/lib/rubocop/dynamic_methods.rb +13 -0
- data/lib/rubocop-dynamic_methods.rb +4 -0
- metadata +92 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module DynamicMethods
|
|
6
|
+
# Finds code evaluated from a string with `class_eval`, `module_eval`,
|
|
7
|
+
# `instance_eval`, or `eval`. A search for `def name` does not find a
|
|
8
|
+
# method a string defines.
|
|
9
|
+
#
|
|
10
|
+
# The autocorrection writes the defs the string holds in place of the
|
|
11
|
+
# call, when the string is a literal or interpolates only the variable
|
|
12
|
+
# of an `each` over a literal list, holds nothing but defs, and is
|
|
13
|
+
# evaluated on the class whose body calls it. A def from a string eval is
|
|
14
|
+
# public even after a bare `private`, so a call after one is refused.
|
|
15
|
+
# Otherwise the offense says what the string defines, or where the code
|
|
16
|
+
# comes from, and why it was not rewritten.
|
|
17
|
+
#
|
|
18
|
+
# The autocorrection is unsafe: it trusts that `class_eval` and
|
|
19
|
+
# `Array#each` are Ruby's own.
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# # bad
|
|
23
|
+
# %w[width height].each do |name|
|
|
24
|
+
# class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
25
|
+
# def #{name} = @size.fetch(:#{name})
|
|
26
|
+
# RUBY
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # good
|
|
30
|
+
# def width = @size.fetch(:width)
|
|
31
|
+
#
|
|
32
|
+
# def height = @size.fetch(:height)
|
|
33
|
+
class StringEval < Base
|
|
34
|
+
extend AutoCorrector
|
|
35
|
+
include FrozenStringLiteral
|
|
36
|
+
include DynamicMethods::Reporting
|
|
37
|
+
|
|
38
|
+
RESTRICT_ON_SEND = %i[class_eval module_eval instance_eval eval].freeze
|
|
39
|
+
|
|
40
|
+
def on_send(node)
|
|
41
|
+
# `instance_eval(&block)` runs a block, whose defs a search finds.
|
|
42
|
+
return if node.block_node || !node.arguments? || node.first_argument.block_pass_type?
|
|
43
|
+
return if node.method?(:eval) && !node.receiver.nil?
|
|
44
|
+
|
|
45
|
+
report(node, node) do
|
|
46
|
+
raise RuboCop::DynamicMethods::Rewrite::Refusal, refusal(node) unless RuboCop::DynamicMethods::Rewrite.statement?(node)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# The lead of a refused offense says what is known about the code:
|
|
53
|
+
# what it evaluates, and what it defines when the string says.
|
|
54
|
+
def message_for(node, plan)
|
|
55
|
+
return super if plan
|
|
56
|
+
|
|
57
|
+
callee = "`#{node.receiver ? "#{node.receiver.source}." : ""}#{node.method_name}`"
|
|
58
|
+
code = node.first_argument
|
|
59
|
+
case code.type
|
|
60
|
+
when :str
|
|
61
|
+
names = defined_names(code.value)
|
|
62
|
+
return "#{callee} evaluates a string that holds no def; run the code without a string, so a reader can search it." if names&.empty?
|
|
63
|
+
return "#{callee} evaluates a string that defines #{names.map { "`#{_1}`" }.join(", ")}; define #{names.size == 1 ? "it" : "them"} with `def`, so a search finds #{names.size == 1 ? "it" : "them"}." if names
|
|
64
|
+
when :dstr
|
|
65
|
+
names = pattern_names(code)
|
|
66
|
+
if names&.any?
|
|
67
|
+
return "#{callee} evaluates a string that defines methods named #{names.map { "`#{_1}`" }.join(", ")}; " \
|
|
68
|
+
"write them with `def`, so a search finds them."
|
|
69
|
+
end
|
|
70
|
+
part = code.children.find { !_1.str_type? }
|
|
71
|
+
return "#{callee} evaluates a string that interpolates `#{part.source}`, so only running it shows which methods it defines." if part
|
|
72
|
+
end
|
|
73
|
+
"#{callee} evaluates code from `#{code.source.lines.first.strip}`, so only running it shows which methods it defines."
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The def names of an interpolated string, with each interpolation
|
|
77
|
+
# kept as written, such as `#{name}?`; nil when they cannot be read.
|
|
78
|
+
def pattern_names(dstr)
|
|
79
|
+
holes = {}
|
|
80
|
+
flat = lambda do |node|
|
|
81
|
+
node.children.map do |part|
|
|
82
|
+
next part.value if part.str_type?
|
|
83
|
+
next flat.call(part) if part.dstr_type?
|
|
84
|
+
return unless part.begin_type?
|
|
85
|
+
|
|
86
|
+
holes["zq#{holes.size}zq"] = part.source
|
|
87
|
+
"zq#{holes.size - 1}zq"
|
|
88
|
+
end.join
|
|
89
|
+
end
|
|
90
|
+
names = defined_names(flat.call(dstr))
|
|
91
|
+
names&.map { |name| name.gsub(/zq\d+zq/) { holes.fetch(_1) } }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The names of the defs in the code, or nil when it is not Ruby.
|
|
95
|
+
def defined_names(code)
|
|
96
|
+
parsed = RuboCop::ProcessedSource.new(code, target_ruby_version, "(eval)")
|
|
97
|
+
return unless parsed.valid_syntax?
|
|
98
|
+
|
|
99
|
+
parsed.ast&.each_node(:def, :defs)&.map { _1.defs_type? ? "self.#{_1.method_name}" : _1.method_name.to_s } || []
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def refusal(node)
|
|
103
|
+
return "`#{node.method_name}` runs the string on another receiver" if %i[eval instance_eval].include?(node.method_name)
|
|
104
|
+
|
|
105
|
+
"it evaluates the string on `#{node.receiver.source}`"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lint_roller"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module DynamicMethods
|
|
8
|
+
# Tells RuboCop where the cops' default configuration is.
|
|
9
|
+
class Plugin < LintRoller::Plugin
|
|
10
|
+
def about
|
|
11
|
+
LintRoller::About.new(
|
|
12
|
+
name: "rubocop-dynamic_methods",
|
|
13
|
+
version: VERSION,
|
|
14
|
+
homepage: "https://github.com/meganemura/rubocop-dynamic_methods",
|
|
15
|
+
description: "Rewrites dynamically defined methods as plain defs you can search for."
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def supported?(context)
|
|
20
|
+
context.engine == :rubocop
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def rules(_context)
|
|
24
|
+
LintRoller::Rules.new(
|
|
25
|
+
type: :path,
|
|
26
|
+
config_format: :rubocop,
|
|
27
|
+
value: Pathname.new(__dir__).join("../../../config/default.yml").expand_path
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|