rubyzen-lint 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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/README.md +110 -0
  4. data/lib/rubyzen/cache/parse_cache.rb +36 -0
  5. data/lib/rubyzen/collections/attributes_collection.rb +32 -0
  6. data/lib/rubyzen/collections/base_collection.rb +30 -0
  7. data/lib/rubyzen/collections/blocks_collection.rb +27 -0
  8. data/lib/rubyzen/collections/call_site_collection.rb +52 -0
  9. data/lib/rubyzen/collections/classes_collection.rb +77 -0
  10. data/lib/rubyzen/collections/constants_collection.rb +11 -0
  11. data/lib/rubyzen/collections/declaration_collection.rb +11 -0
  12. data/lib/rubyzen/collections/file_collection.rb +81 -0
  13. data/lib/rubyzen/collections/macros_collection.rb +11 -0
  14. data/lib/rubyzen/collections/methods_collection.rb +67 -0
  15. data/lib/rubyzen/collections/modules_collection.rb +36 -0
  16. data/lib/rubyzen/collections/parameters_collection.rb +11 -0
  17. data/lib/rubyzen/collections/raises_collection.rb +26 -0
  18. data/lib/rubyzen/collections/requires_collection.rb +32 -0
  19. data/lib/rubyzen/collections/rescues_collection.rb +19 -0
  20. data/lib/rubyzen/declarations/attribute_declaration.rb +62 -0
  21. data/lib/rubyzen/declarations/block_declaration.rb +49 -0
  22. data/lib/rubyzen/declarations/call_site_declaration.rb +98 -0
  23. data/lib/rubyzen/declarations/class_declaration.rb +168 -0
  24. data/lib/rubyzen/declarations/constant_declaration.rb +155 -0
  25. data/lib/rubyzen/declarations/file_declaration.rb +69 -0
  26. data/lib/rubyzen/declarations/if_statement_declaration.rb +44 -0
  27. data/lib/rubyzen/declarations/macro_declaration.rb +81 -0
  28. data/lib/rubyzen/declarations/method_declaration.rb +63 -0
  29. data/lib/rubyzen/declarations/module_declaration.rb +115 -0
  30. data/lib/rubyzen/declarations/parameter_declaration.rb +43 -0
  31. data/lib/rubyzen/declarations/raise_declaration.rb +87 -0
  32. data/lib/rubyzen/declarations/require_declaration.rb +61 -0
  33. data/lib/rubyzen/declarations/rescue_declaration.rb +54 -0
  34. data/lib/rubyzen/lint.rb +1 -0
  35. data/lib/rubyzen/matchers/matcher_helpers.rb +176 -0
  36. data/lib/rubyzen/matchers/zen_empty_matcher.rb +54 -0
  37. data/lib/rubyzen/matchers/zen_false_matcher.rb +63 -0
  38. data/lib/rubyzen/matchers/zen_true_matcher.rb +57 -0
  39. data/lib/rubyzen/parsers/a_s_t_parser.rb +33 -0
  40. data/lib/rubyzen/project.rb +69 -0
  41. data/lib/rubyzen/providers/attributes_provider.rb +19 -0
  42. data/lib/rubyzen/providers/blocks_provider.rb +15 -0
  43. data/lib/rubyzen/providers/call_site_provider.rb +15 -0
  44. data/lib/rubyzen/providers/class_name_provider.rb +36 -0
  45. data/lib/rubyzen/providers/collection_filter_provider.rb +64 -0
  46. data/lib/rubyzen/providers/constants_provider.rb +17 -0
  47. data/lib/rubyzen/providers/file_path_provider.rb +26 -0
  48. data/lib/rubyzen/providers/if_statements_provider.rb +11 -0
  49. data/lib/rubyzen/providers/line_number_provider.rb +11 -0
  50. data/lib/rubyzen/providers/lines_of_code_provider.rb +11 -0
  51. data/lib/rubyzen/providers/macros_provider.rb +17 -0
  52. data/lib/rubyzen/providers/raises_provider.rb +19 -0
  53. data/lib/rubyzen/providers/requires_provider.rb +19 -0
  54. data/lib/rubyzen/providers/rescues_provider.rb +17 -0
  55. data/lib/rubyzen/providers/source_code_provider.rb +11 -0
  56. data/lib/rubyzen/providers/visibility_provider.rb +49 -0
  57. data/lib/rubyzen/version.rb +3 -0
  58. data/lib/rubyzen-lint.rb +1 -0
  59. data/lib/rubyzen.rb +98 -0
  60. data/rubyzen-lint.gemspec +28 -0
  61. metadata +148 -0
@@ -0,0 +1,155 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents a constant assignment (+MAX = 100+) or reference (+MAX+).
4
+ #
5
+ # @example
6
+ # const = file.constants.filter(&:assignment?).first
7
+ # const.name #=> "MAX"
8
+ # const.value #=> 100
9
+ # const.top_level? #=> true
10
+ #
11
+ class ConstantDeclaration
12
+ include Rubyzen::Providers::FilePathProvider
13
+ include Rubyzen::Providers::LineNumberProvider
14
+ include Rubyzen::Providers::ClassNameProvider
15
+ include Rubyzen::Providers::SourceCodeProvider
16
+
17
+ # @return [RuboCop::AST::Node]
18
+ attr_reader :node
19
+
20
+ # @return [FileDeclaration, ClassDeclaration, ModuleDeclaration]
21
+ attr_reader :parent
22
+
23
+ # @param node [RuboCop::AST::Node] the AST node
24
+ # @param parent [FileDeclaration, ClassDeclaration, ModuleDeclaration] the parent declaration
25
+ def initialize(node, parent)
26
+ @node = node
27
+ @parent = parent
28
+ end
29
+
30
+ # Returns the constant name.
31
+ #
32
+ # @return [String]
33
+ def name
34
+ case node.type
35
+ when :casgn
36
+ node.children[1].to_s
37
+ when :const
38
+ node.const_name
39
+ end
40
+ end
41
+
42
+ # Returns the assigned value for constant assignments.
43
+ #
44
+ # @return [String, Integer, Float, Boolean, nil]
45
+ def value
46
+ return nil unless assignment?
47
+
48
+ value_node = node.children[2]
49
+ return nil unless value_node
50
+
51
+ case value_node.type
52
+ when :str
53
+ value_node.str_content
54
+ when :int
55
+ value_node.children[0]
56
+ when :float
57
+ value_node.children[0]
58
+ when :true, :false
59
+ value_node.type == :true
60
+ else
61
+ value_node.source
62
+ end
63
+ end
64
+
65
+ # Returns whether this is a constant assignment (+:casgn+).
66
+ #
67
+ # @return [Boolean]
68
+ def assignment?
69
+ node.type == :casgn
70
+ end
71
+
72
+ # Returns whether this is a constant reference (+:const+).
73
+ #
74
+ # @return [Boolean]
75
+ def reference?
76
+ node.type == :const
77
+ end
78
+
79
+ # Returns whether this constant is defined at file scope (not inside a class or module).
80
+ #
81
+ # @return [Boolean]
82
+ def top_level?
83
+ return false unless parent.is_a?(Rubyzen::Declarations::FileDeclaration)
84
+
85
+ current_node = node
86
+ while current_node
87
+ current_node = current_node.parent
88
+ return false if current_node && (current_node.type == :class || current_node.type == :module)
89
+ end
90
+
91
+ true
92
+ end
93
+
94
+ # Returns the enclosing {ClassDeclaration}, if any.
95
+ #
96
+ # @return [ClassDeclaration, nil]
97
+ def enclosing_class
98
+ find_enclosing_ast_node(:class) do |n|
99
+ Rubyzen::Declarations::ClassDeclaration.new(n, file_declaration)
100
+ end
101
+ end
102
+
103
+ # Returns whether this constant is defined inside a class.
104
+ #
105
+ # @return [Boolean]
106
+ def in_class?
107
+ !enclosing_class.nil?
108
+ end
109
+
110
+ # Returns the enclosing {ModuleDeclaration}, if any.
111
+ #
112
+ # @return [ModuleDeclaration, nil]
113
+ def enclosing_module
114
+ find_enclosing_ast_node(:module) do |n|
115
+ Rubyzen::Declarations::ModuleDeclaration.new(n, file_declaration)
116
+ end
117
+ end
118
+
119
+ # Returns whether this constant is defined inside a module.
120
+ #
121
+ # @return [Boolean]
122
+ def in_module?
123
+ !enclosing_module.nil?
124
+ end
125
+
126
+ # Returns whether this constant is defined inside a class or module.
127
+ #
128
+ # @return [Boolean]
129
+ def scoped?
130
+ !top_level?
131
+ end
132
+
133
+ private
134
+
135
+ def file_declaration
136
+ current = parent
137
+ while current
138
+ return current if current.is_a?(Rubyzen::Declarations::FileDeclaration)
139
+ return current.file_declaration if current.respond_to?(:file_declaration)
140
+ current = current.respond_to?(:parent) ? current.parent : nil
141
+ end
142
+ nil
143
+ end
144
+
145
+ def find_enclosing_ast_node(type)
146
+ current_node = node.parent
147
+ while current_node
148
+ return yield(current_node) if current_node.type == type
149
+ current_node = current_node.respond_to?(:parent) ? current_node.parent : nil
150
+ end
151
+ nil
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,69 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents a parsed Ruby source file. This is the root of the declaration
4
+ # hierarchy — all other declarations are accessed through a FileDeclaration.
5
+ #
6
+ # @example
7
+ # project = Rubyzen::Project.new("/app/src")
8
+ # file = project.files.first
9
+ # file.name #=> "user.rb"
10
+ # file.classes #=> [ClassDeclaration, ...]
11
+ #
12
+ class FileDeclaration
13
+ include Rubyzen::Providers::FilePathProvider
14
+ include Rubyzen::Providers::LineNumberProvider
15
+ include Rubyzen::Providers::LinesOfCodeProvider
16
+ include Rubyzen::Providers::ConstantsProvider
17
+ include Rubyzen::Providers::RequiresProvider
18
+ include Rubyzen::Providers::CallSiteProvider
19
+ include Rubyzen::Providers::BlocksProvider
20
+
21
+ # @return [String] absolute path to the source file
22
+ attr_reader :path
23
+
24
+ # @return [RuboCop::AST::Node] the root AST node
25
+ attr_reader :node
26
+ alias :ast :node
27
+
28
+ # @param path [String] absolute file path
29
+ # @param ast [RuboCop::AST::Node] parsed AST root node
30
+ def initialize(path, ast)
31
+ @path = path
32
+ @node = ast
33
+ end
34
+
35
+ # Returns the basename of the file.
36
+ #
37
+ # @return [String] e.g. +"user.rb"+
38
+ def name
39
+ File.basename(path)
40
+ end
41
+
42
+ # Returns all classes defined in this file.
43
+ #
44
+ # @return [Array<ClassDeclaration>]
45
+ def classes
46
+ node.each_node(:class).map do |class_node|
47
+ ClassDeclaration.new(class_node, self)
48
+ end
49
+ end
50
+
51
+ # Returns the name of the first module in the file, used to determine
52
+ # the top-level namespace.
53
+ #
54
+ # @return [String, nil]
55
+ def top_level_module_name
56
+ modules.first&.name_without_modules
57
+ end
58
+
59
+ # Returns all modules defined in this file.
60
+ #
61
+ # @return [Array<ModuleDeclaration>]
62
+ def modules
63
+ node.each_node(:module).map do |module_node|
64
+ Rubyzen::Declarations::ModuleDeclaration.new(module_node, self)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,44 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents an +if+ / +unless+ statement within a method or class.
4
+ #
5
+ # @example
6
+ # if_stmt = method.if_statements.first
7
+ # if_stmt.condition_source #=> "user.active?"
8
+ # if_stmt.source_code #=> "if user.active?\n ..."
9
+ #
10
+ class IfStatementDeclaration
11
+ include Rubyzen::Providers::FilePathProvider
12
+ include Rubyzen::Providers::ClassNameProvider
13
+ include Rubyzen::Providers::LineNumberProvider
14
+ include Rubyzen::Providers::SourceCodeProvider
15
+
16
+ # @return [RuboCop::AST::Node]
17
+ attr_reader :node
18
+
19
+ # @return [MethodDeclaration, ClassDeclaration]
20
+ attr_reader :parent
21
+
22
+ # @param node [RuboCop::AST::Node] the AST node
23
+ # @param parent [MethodDeclaration, ClassDeclaration] the parent declaration
24
+ def initialize(node, parent)
25
+ @node = node
26
+ @parent = parent
27
+ end
28
+
29
+ # Returns the raw source of the condition expression.
30
+ #
31
+ # @return [String, nil]
32
+ def condition_source
33
+ node.condition&.source
34
+ end
35
+
36
+ # Returns the name of the parent declaration.
37
+ #
38
+ # @return [String]
39
+ def name
40
+ parent.name
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,81 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents a class-level macro call (e.g. +validates_required+, +belongs_to+).
4
+ #
5
+ # @example
6
+ # macro = klass.macros.first
7
+ # macro.name #=> "validates_required"
8
+ # macro.symbols #=> [:name, :email]
9
+ # macro.keyword_args #=> [:foreign_key, :optional]
10
+ #
11
+ class MacroDeclaration
12
+ include Rubyzen::Providers::FilePathProvider
13
+ include Rubyzen::Providers::ClassNameProvider
14
+ include Rubyzen::Providers::LineNumberProvider
15
+ include Rubyzen::Providers::SourceCodeProvider
16
+
17
+ # @return [RuboCop::AST::Node]
18
+ attr_reader :node
19
+
20
+ # @return [ClassDeclaration]
21
+ attr_reader :parent
22
+
23
+ # @param node [RuboCop::AST::Node] the AST node
24
+ # @param parent [ClassDeclaration] the parent declaration
25
+ def initialize(node, parent)
26
+ @node = node
27
+ @parent = parent
28
+ end
29
+
30
+ # Returns the macro method name.
31
+ #
32
+ # @return [String] e.g. +"validates_required"+, +"belongs_to"+
33
+ def name
34
+ node.method_name.to_s
35
+ end
36
+
37
+ # Returns positional symbol arguments.
38
+ #
39
+ # @return [Array<Symbol>]
40
+ def symbols
41
+ node.arguments.select { |arg| arg.type == :sym }.map(&:value)
42
+ end
43
+
44
+ # Returns positional string arguments.
45
+ #
46
+ # @return [Array<String>]
47
+ def strings
48
+ node.arguments.select { |arg| arg.type == :str }.map(&:value)
49
+ end
50
+
51
+ # Returns keyword argument keys.
52
+ #
53
+ # @return [Array<Symbol>]
54
+ def keyword_args
55
+ extract_keyword_args(node)
56
+ end
57
+
58
+ # Returns the constant receiver, if any.
59
+ #
60
+ # @return [String, nil] e.g. +"Config"+ for +Config.setting+
61
+ def receiver
62
+ node.receiver&.type == :const ? node.receiver.const_name : nil
63
+ end
64
+
65
+ private
66
+
67
+ def extract_keyword_args(send_node)
68
+ send_node.arguments.flat_map do |arg|
69
+ if arg.hash_type?
70
+ arg.each_pair.map do |pair|
71
+ key_node = pair.key
72
+ key_node.type == :sym ? key_node.value : nil
73
+ end.compact
74
+ else
75
+ []
76
+ end
77
+ end.uniq
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,63 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents a Ruby method definition (+def+ or +def self.+).
4
+ #
5
+ # @example
6
+ # method = klass.instance_methods.first
7
+ # method.name #=> "calculate"
8
+ # method.parameters? #=> true
9
+ # method.call_sites #=> CallSiteCollection
10
+ # method.visibility #=> :private
11
+ #
12
+ class MethodDeclaration
13
+ include Rubyzen::Providers::IfStatementsProvider
14
+ include Rubyzen::Providers::BlocksProvider
15
+ include Rubyzen::Providers::FilePathProvider
16
+ include Rubyzen::Providers::ClassNameProvider
17
+ include Rubyzen::Providers::LineNumberProvider
18
+ include Rubyzen::Providers::ConstantsProvider
19
+ include Rubyzen::Providers::CallSiteProvider
20
+ include Rubyzen::Providers::LinesOfCodeProvider
21
+ include Rubyzen::Providers::VisibilityProvider
22
+ include Rubyzen::Providers::RescuesProvider
23
+ include Rubyzen::Providers::RaisesProvider
24
+
25
+ # @return [RuboCop::AST::Node]
26
+ attr_reader :node
27
+
28
+ # @return [ClassDeclaration, ModuleDeclaration]
29
+ attr_reader :parent_class
30
+ alias :parent :parent_class
31
+
32
+ def initialize(node, parent_class)
33
+ @node = node
34
+ @parent_class = parent_class
35
+ end
36
+
37
+ # Returns the method name.
38
+ #
39
+ # @return [String]
40
+ def name
41
+ node.method_name.to_s
42
+ end
43
+
44
+ # Returns the method's parameters.
45
+ #
46
+ # @return [Collections::ParametersCollection]
47
+ def parameters
48
+ Collections::ParametersCollection.new(
49
+ node.arguments.map do |arg|
50
+ ParameterDeclaration.new(arg, self)
51
+ end
52
+ )
53
+ end
54
+
55
+ # Returns whether this method has any parameters.
56
+ #
57
+ # @return [Boolean]
58
+ def parameters?
59
+ node.arguments.any?
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,115 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents a Ruby module definition.
4
+ #
5
+ # @example
6
+ # mod = file.modules.first
7
+ # mod.name #=> "Admin::Api"
8
+ # mod.all_methods #=> MethodsCollection
9
+ # mod.classes #=> [ClassDeclaration, ...]
10
+ #
11
+ class ModuleDeclaration
12
+ include Rubyzen::Providers::FilePathProvider
13
+ include Rubyzen::Providers::LineNumberProvider
14
+ include Rubyzen::Providers::ClassNameProvider
15
+ include Rubyzen::Providers::ConstantsProvider
16
+ include Rubyzen::Providers::LinesOfCodeProvider
17
+ include Rubyzen::Providers::AttributesProvider
18
+
19
+ # @return [RuboCop::AST::Node]
20
+ attr_reader :node
21
+
22
+ # @return [FileDeclaration]
23
+ attr_reader :file_declaration
24
+
25
+ # @param node [RuboCop::AST::Node] the AST node
26
+ # @param file_declaration [FileDeclaration] the parent file declaration
27
+ def initialize(node, file_declaration)
28
+ @node = node
29
+ @file_declaration = file_declaration
30
+ end
31
+
32
+ # Returns the fully-qualified module name including parent modules.
33
+ #
34
+ # @return [String] e.g. +"Admin::Api"+
35
+ def name
36
+ parent_module_names = []
37
+ current_node = node.parent
38
+
39
+ while current_node
40
+ if current_node.type == :module
41
+ parent_module_names.unshift(current_node.identifier&.const_name)
42
+ end
43
+ current_node = current_node.parent
44
+ end
45
+
46
+ [parent_module_names, name_without_modules].flatten.compact.join('::')
47
+ end
48
+
49
+ # Returns the module name without parent module prefixes.
50
+ #
51
+ # @return [String]
52
+ def name_without_modules
53
+ node.identifier&.const_name
54
+ end
55
+
56
+ # Returns nested modules within this module.
57
+ #
58
+ # @return [Array<ModuleDeclaration>]
59
+ def modules
60
+ node.each_descendant(:module).map { |mod_node| ModuleDeclaration.new(mod_node, file_declaration) }
61
+ end
62
+
63
+ # Returns classes defined within this module.
64
+ #
65
+ # @return [Array<ClassDeclaration>]
66
+ def classes
67
+ node.each_node(:class).map do |class_node|
68
+ ClassDeclaration.new(class_node, file_declaration)
69
+ end
70
+ end
71
+
72
+ # Returns methods defined directly in this module.
73
+ #
74
+ # @return [Collections::MethodsCollection]
75
+ def all_methods
76
+ Collections::MethodsCollection.new(
77
+ direct_method_nodes.map { |method_node| MethodDeclaration.new(method_node, self) }
78
+ )
79
+ end
80
+
81
+ private
82
+
83
+ def module_body_node
84
+ node.children[1]
85
+ end
86
+
87
+ def module_body_children
88
+ body = module_body_node
89
+ return [] unless body
90
+
91
+ body.type == :begin ? body.child_nodes : [body]
92
+ end
93
+
94
+ def direct_method_nodes
95
+ direct_nodes = module_body_children.select do |child|
96
+ %i[def defs].include?(child.type)
97
+ end
98
+ singleton_nodes = module_body_children.flat_map do |child|
99
+ next [] unless child.type == :sclass
100
+ next [] unless child.children[0]&.type == :self
101
+
102
+ sclass_body = child.children[1]
103
+ next [] unless sclass_body
104
+
105
+ sclass_children = sclass_body.type == :begin ? sclass_body.child_nodes : [sclass_body]
106
+ sclass_children.select do |sclass_child|
107
+ %i[def defs].include?(sclass_child.type)
108
+ end
109
+ end
110
+
111
+ direct_nodes + singleton_nodes
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,43 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents a method parameter.
4
+ #
5
+ # @example
6
+ # param = method.parameters.first
7
+ # param.name #=> :user_id
8
+ # param.default_value #=> 42
9
+ #
10
+ class ParameterDeclaration
11
+ include Rubyzen::Providers::FilePathProvider
12
+ include Rubyzen::Providers::LineNumberProvider
13
+ include Rubyzen::Providers::ClassNameProvider
14
+
15
+ # @return [RuboCop::AST::Node]
16
+ attr_reader :node
17
+
18
+ # @return [MethodDeclaration]
19
+ attr_reader :parent
20
+
21
+ # @param node [RuboCop::AST::Node] the AST node
22
+ # @param parent [MethodDeclaration] the parent declaration
23
+ def initialize(node, parent)
24
+ @node = node
25
+ @parent = parent
26
+ end
27
+
28
+ # Returns the parameter name.
29
+ #
30
+ # @return [Symbol]
31
+ def name
32
+ node.name
33
+ end
34
+
35
+ # Returns the default value if one is defined.
36
+ #
37
+ # @return [Object, nil]
38
+ def default_value
39
+ node.children[1]&.value
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,87 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents a +raise+ statement.
4
+ #
5
+ # @example
6
+ # raise_decl = method.raises.first
7
+ # raise_decl.exception_types #=> ["ArgumentError"]
8
+ # raise_decl.message #=> "invalid input"
9
+ # raise_decl.with_string? #=> false
10
+ #
11
+ class RaiseDeclaration
12
+ include Rubyzen::Providers::FilePathProvider
13
+ include Rubyzen::Providers::LineNumberProvider
14
+ include Rubyzen::Providers::ClassNameProvider
15
+ include Rubyzen::Providers::SourceCodeProvider
16
+
17
+ # @return [RuboCop::AST::Node]
18
+ attr_reader :node
19
+
20
+ # @return [MethodDeclaration, BlockDeclaration]
21
+ attr_reader :parent
22
+
23
+ # @param node [RuboCop::AST::Node] the AST node
24
+ # @param parent [MethodDeclaration, BlockDeclaration] the parent declaration
25
+ def initialize(node, parent)
26
+ @node = node
27
+ @parent = parent
28
+ end
29
+
30
+ # Returns the exception class names being raised.
31
+ # Defaults to +["RuntimeError"]+ for bare +raise+ or +raise "message"+.
32
+ #
33
+ # @return [Array<String>]
34
+ def exception_types
35
+ extract_exception_types
36
+ end
37
+
38
+ # Returns whether the raise uses a bare string (+raise "message"+).
39
+ #
40
+ # @return [Boolean]
41
+ def with_string?
42
+ first_arg = node.arguments.first
43
+ first_arg&.type == :str
44
+ end
45
+
46
+ # Returns the error message string, if any.
47
+ #
48
+ # @return [String, nil]
49
+ def message
50
+ extract_message
51
+ end
52
+
53
+ private
54
+
55
+ def extract_exception_types
56
+ first_arg = node.arguments.first
57
+
58
+ return ['RuntimeError'] if first_arg.nil? || first_arg.type == :str
59
+
60
+ if first_arg.type == :const
61
+ [first_arg.const_name]
62
+ elsif first_arg.type == :send && first_arg.method_name == :new
63
+ receiver = first_arg.receiver
64
+ receiver&.type == :const ? [receiver.const_name] : ['RuntimeError']
65
+ else
66
+ ['RuntimeError']
67
+ end
68
+ end
69
+
70
+ def extract_message
71
+ first_arg = node.arguments.first
72
+ second_arg = node.arguments[1]
73
+
74
+ if first_arg&.type == :str
75
+ first_arg.value
76
+ elsif second_arg&.type == :str
77
+ second_arg.value
78
+ elsif first_arg&.type == :send && first_arg.method_name == :new
79
+ msg_arg = first_arg.arguments.first
80
+ msg_arg&.type == :str ? msg_arg.value : nil
81
+ else
82
+ nil
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end