igata 0.2.1 → 0.3.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 +4 -4
- data/CHANGELOG.md +83 -0
- data/QUICKSTART.md +37 -0
- data/README.md +104 -1
- data/Rakefile +135 -0
- data/USAGE.md +441 -0
- data/exe/igata +2 -2
- data/lib/igata/extractors/argument_extractor.rb +99 -0
- data/lib/igata/extractors/boundary_value_generator.rb +276 -0
- data/lib/igata/extractors/branch_analyzer.rb +19 -1
- data/lib/igata/extractors/comparison_analyzer.rb +19 -1
- data/lib/igata/extractors/constant_path.rb +29 -9
- data/lib/igata/extractors/exception_analyzer.rb +172 -0
- data/lib/igata/extractors/method_names.rb +15 -1
- data/lib/igata/formatters/base.rb +17 -1
- data/lib/igata/formatters/minitest.rb +0 -17
- data/lib/igata/formatters/minitest_spec.rb +15 -0
- data/lib/igata/formatters/rspec.rb +0 -17
- data/lib/igata/formatters/templates/minitest/method.erb +36 -0
- data/lib/igata/formatters/templates/minitest_spec/class.erb +13 -0
- data/lib/igata/formatters/templates/minitest_spec/method.erb +47 -0
- data/lib/igata/formatters/templates/rspec/method.erb +36 -0
- data/lib/igata/values.rb +35 -5
- data/lib/igata/version.rb +1 -1
- data/lib/igata.rb +46 -6
- metadata +10 -3
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Igata
|
|
4
|
+
module Extractors
|
|
5
|
+
# rubocop:disable Metrics/ClassLength, Lint/DuplicateBranch, Naming/PredicateMethod
|
|
6
|
+
class BoundaryValueGenerator
|
|
7
|
+
def self.extract(comparisons)
|
|
8
|
+
new(comparisons).extract
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(comparisons)
|
|
12
|
+
@comparisons = comparisons
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def extract
|
|
16
|
+
@comparisons.map do |comparison|
|
|
17
|
+
generate_boundary_values(comparison)
|
|
18
|
+
end.compact
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def generate_boundary_values(comparison) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
24
|
+
right_value = comparison.right
|
|
25
|
+
|
|
26
|
+
# Determine value type and generate appropriate boundary values
|
|
27
|
+
test_values, description = if numeric?(right_value)
|
|
28
|
+
generate_numeric_boundaries(comparison)
|
|
29
|
+
elsif string_literal?(right_value)
|
|
30
|
+
generate_string_boundaries(comparison)
|
|
31
|
+
elsif nil_value?(right_value)
|
|
32
|
+
generate_nil_boundaries(comparison)
|
|
33
|
+
elsif boolean_value?(right_value)
|
|
34
|
+
generate_boolean_boundaries(comparison)
|
|
35
|
+
elsif symbol_value?(right_value)
|
|
36
|
+
generate_symbol_boundaries(comparison)
|
|
37
|
+
else
|
|
38
|
+
# Variable or method call - skip boundary value generation
|
|
39
|
+
return nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return nil if test_values.nil? || test_values.empty?
|
|
43
|
+
|
|
44
|
+
Values::BoundaryValueInfo.new(
|
|
45
|
+
comparison: comparison,
|
|
46
|
+
test_values: test_values,
|
|
47
|
+
description: description
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Numeric boundary values
|
|
52
|
+
# rubocop:disable Metrics/MethodLength
|
|
53
|
+
def generate_numeric_boundaries(comparison)
|
|
54
|
+
value = parse_numeric(comparison.right)
|
|
55
|
+
operator = comparison.operator
|
|
56
|
+
|
|
57
|
+
test_values = case operator
|
|
58
|
+
when :>=
|
|
59
|
+
[value - 1, value, value + 1]
|
|
60
|
+
when :>
|
|
61
|
+
[value, value + 1]
|
|
62
|
+
when :<=
|
|
63
|
+
[value - 1, value, value + 1]
|
|
64
|
+
when :<
|
|
65
|
+
[value - 1, value]
|
|
66
|
+
when :==
|
|
67
|
+
[value - 1, value, value + 1]
|
|
68
|
+
when :!=
|
|
69
|
+
[value]
|
|
70
|
+
else
|
|
71
|
+
[]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
description = build_numeric_description(operator, value, test_values)
|
|
75
|
+
[test_values, description]
|
|
76
|
+
end
|
|
77
|
+
# rubocop:enable Metrics/MethodLength
|
|
78
|
+
|
|
79
|
+
# String literal boundary values
|
|
80
|
+
# rubocop:disable Metrics/MethodLength
|
|
81
|
+
def generate_string_boundaries(comparison)
|
|
82
|
+
str_value = parse_string(comparison.right)
|
|
83
|
+
operator = comparison.operator
|
|
84
|
+
|
|
85
|
+
test_values = case operator
|
|
86
|
+
when :>=, :>
|
|
87
|
+
# For string comparison: previous, same, next
|
|
88
|
+
[decrement_string(str_value), str_value, increment_string(str_value)]
|
|
89
|
+
when :<=, :<
|
|
90
|
+
[decrement_string(str_value), str_value, increment_string(str_value)]
|
|
91
|
+
when :==
|
|
92
|
+
[str_value, "different_string"]
|
|
93
|
+
when :!=
|
|
94
|
+
[str_value, "different_string"]
|
|
95
|
+
else
|
|
96
|
+
[]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
description = build_string_description(operator, str_value, test_values)
|
|
100
|
+
[test_values, description]
|
|
101
|
+
end
|
|
102
|
+
# rubocop:enable Metrics/MethodLength
|
|
103
|
+
|
|
104
|
+
# Nil boundary values
|
|
105
|
+
# rubocop:disable Metrics/MethodLength
|
|
106
|
+
def generate_nil_boundaries(comparison)
|
|
107
|
+
operator = comparison.operator
|
|
108
|
+
|
|
109
|
+
test_values = case operator
|
|
110
|
+
when :==
|
|
111
|
+
[nil, "some_value"]
|
|
112
|
+
when :!=
|
|
113
|
+
[nil, "some_value"]
|
|
114
|
+
else
|
|
115
|
+
[]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
description = "nil check: test with #{test_values.inspect}"
|
|
119
|
+
[test_values, description]
|
|
120
|
+
end
|
|
121
|
+
# rubocop:enable Metrics/MethodLength
|
|
122
|
+
|
|
123
|
+
# Boolean boundary values
|
|
124
|
+
def generate_boolean_boundaries(comparison)
|
|
125
|
+
operator = comparison.operator
|
|
126
|
+
|
|
127
|
+
test_values = case operator
|
|
128
|
+
when :==, :!=
|
|
129
|
+
[true, false]
|
|
130
|
+
else
|
|
131
|
+
[]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
description = "boolean check: test with #{test_values.inspect}"
|
|
135
|
+
[test_values, description]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Symbol boundary values
|
|
139
|
+
# rubocop:disable Metrics/MethodLength
|
|
140
|
+
def generate_symbol_boundaries(comparison)
|
|
141
|
+
sym_value = parse_symbol(comparison.right)
|
|
142
|
+
operator = comparison.operator
|
|
143
|
+
|
|
144
|
+
test_values = case operator
|
|
145
|
+
when :==
|
|
146
|
+
[sym_value, ":other_symbol"]
|
|
147
|
+
when :!=
|
|
148
|
+
[sym_value, ":other_symbol"]
|
|
149
|
+
else
|
|
150
|
+
[]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
description = "symbol check: test with #{test_values.inspect}"
|
|
154
|
+
[test_values, description]
|
|
155
|
+
end
|
|
156
|
+
# rubocop:enable Metrics/MethodLength
|
|
157
|
+
|
|
158
|
+
# Type detection methods
|
|
159
|
+
def numeric?(str)
|
|
160
|
+
# Check if string is a numeric literal (integer or float)
|
|
161
|
+
return false if str.nil? || str.empty?
|
|
162
|
+
|
|
163
|
+
# Remove quotes if present (shouldn't be for numbers but just in case)
|
|
164
|
+
clean_str = str.gsub(/["']/, "")
|
|
165
|
+
# Match integer or float
|
|
166
|
+
clean_str.match?(/\A-?\d+(\.\d+)?\z/)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def string_literal?(str)
|
|
170
|
+
# Check if string is a string literal (starts and ends with quotes)
|
|
171
|
+
return false if str.nil? || str.empty?
|
|
172
|
+
|
|
173
|
+
str.match?(/\A["'].*["']\z/)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def nil_value?(str)
|
|
177
|
+
str == "nil"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def boolean_value?(str)
|
|
181
|
+
%w[true false].include?(str)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def symbol_value?(str)
|
|
185
|
+
return false if str.nil? || str.empty?
|
|
186
|
+
|
|
187
|
+
str.start_with?(":")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Parsing methods
|
|
191
|
+
def parse_numeric(str)
|
|
192
|
+
clean_str = str.gsub(/["']/, "")
|
|
193
|
+
clean_str.include?(".") ? clean_str.to_f : clean_str.to_i
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def parse_string(str)
|
|
197
|
+
# Remove surrounding quotes
|
|
198
|
+
str.gsub(/\A["']|["']\z/, "")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def parse_boolean(str)
|
|
202
|
+
str == "true"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def parse_symbol(str)
|
|
206
|
+
str # Keep as is with colon
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# String manipulation for boundary values
|
|
210
|
+
# rubocop:disable Metrics/MethodLength
|
|
211
|
+
def increment_string(str)
|
|
212
|
+
# Simple increment: "A" -> "B", "Z" -> "AA"
|
|
213
|
+
if str.empty?
|
|
214
|
+
"A"
|
|
215
|
+
elsif str == "Z"
|
|
216
|
+
"AA"
|
|
217
|
+
else
|
|
218
|
+
last_char = str[-1]
|
|
219
|
+
if last_char == "z"
|
|
220
|
+
"#{str[0..-2]}aa"
|
|
221
|
+
else
|
|
222
|
+
str[0..-2] + last_char.next
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
# rubocop:enable Metrics/MethodLength
|
|
227
|
+
|
|
228
|
+
def decrement_string(str)
|
|
229
|
+
# Simple decrement: "B" -> "A", "A" -> ""
|
|
230
|
+
return "" if str.empty?
|
|
231
|
+
return "" if str == "A"
|
|
232
|
+
|
|
233
|
+
last_char = str[-1]
|
|
234
|
+
if last_char == "a"
|
|
235
|
+
str[0..-2]
|
|
236
|
+
else
|
|
237
|
+
str[0..-2] + (last_char.ord - 1).chr
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Description builders
|
|
242
|
+
def build_numeric_description(operator, value, test_values) # rubocop:disable Metrics/MethodLength
|
|
243
|
+
case operator
|
|
244
|
+
when :>=
|
|
245
|
+
"#{value - 1} (below), #{value} (boundary), #{value + 1} (above)"
|
|
246
|
+
when :>
|
|
247
|
+
"#{value} (boundary), #{value + 1} (above)"
|
|
248
|
+
when :<=
|
|
249
|
+
"#{value - 1} (below), #{value} (boundary), #{value + 1} (above)"
|
|
250
|
+
when :<
|
|
251
|
+
"#{value - 1} (below), #{value} (boundary)"
|
|
252
|
+
when :==
|
|
253
|
+
"#{value - 1} (not equal), #{value} (equal), #{value + 1} (not equal)"
|
|
254
|
+
when :!=
|
|
255
|
+
"#{value} (equal)"
|
|
256
|
+
else
|
|
257
|
+
test_values.inspect
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def build_string_description(operator, str_value, test_values)
|
|
262
|
+
case operator
|
|
263
|
+
when :>=, :>, :<=, :<
|
|
264
|
+
"string comparison: #{test_values.inspect}"
|
|
265
|
+
when :==
|
|
266
|
+
"equal: #{str_value.inspect}, not equal: different string"
|
|
267
|
+
when :!=
|
|
268
|
+
"equal: #{str_value.inspect}, not equal: different string"
|
|
269
|
+
else
|
|
270
|
+
test_values.inspect
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
# rubocop:enable Metrics/ClassLength, Lint/DuplicateBranch, Naming/PredicateMethod
|
|
275
|
+
end
|
|
276
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
class Igata
|
|
4
4
|
module Extractors
|
|
5
|
+
# rubocop:disable Metrics/ClassLength, Lint/DuplicateBranch
|
|
5
6
|
class BranchAnalyzer
|
|
6
7
|
def self.extract(method_node)
|
|
7
8
|
new(method_node).extract
|
|
@@ -54,6 +55,22 @@ class Igata
|
|
|
54
55
|
# Traverse when branches
|
|
55
56
|
traverse_node(node.body, branches) if node.respond_to?(:body)
|
|
56
57
|
traverse_node(node.else, branches) if node.respond_to?(:else)
|
|
58
|
+
# Handle RescueNode (container node for rescue clauses)
|
|
59
|
+
elsif node.is_a?(Kanayago::RescueNode)
|
|
60
|
+
# Traverse head (main body before rescue)
|
|
61
|
+
if node.respond_to?(:head) && node.head
|
|
62
|
+
if node.head.is_a?(Array)
|
|
63
|
+
node.head.each { |child| traverse_node(child, branches) }
|
|
64
|
+
else
|
|
65
|
+
traverse_node(node.head, branches)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
traverse_node(node.resq, branches) if node.respond_to?(:resq)
|
|
69
|
+
traverse_node(node.else, branches) if node.respond_to?(:else)
|
|
70
|
+
# Handle RescueBodyNode
|
|
71
|
+
elsif node.is_a?(Kanayago::RescueBodyNode)
|
|
72
|
+
traverse_node(node.body, branches) if node.respond_to?(:body)
|
|
73
|
+
traverse_node(node.next, branches) if node.respond_to?(:next)
|
|
57
74
|
# Handle ScopeNode (container node)
|
|
58
75
|
elsif node.is_a?(Kanayago::ScopeNode)
|
|
59
76
|
traverse_node(node.body, branches) if node.respond_to?(:body)
|
|
@@ -92,7 +109,7 @@ class Igata
|
|
|
92
109
|
elsif node.is_a?(Kanayago::IntegerNode)
|
|
93
110
|
node.val.to_s
|
|
94
111
|
elsif node.is_a?(Kanayago::StringNode)
|
|
95
|
-
"\"#{node.
|
|
112
|
+
"\"#{node.ptr}\""
|
|
96
113
|
elsif node.is_a?(Kanayago::SymbolNode)
|
|
97
114
|
":#{node.ptr}"
|
|
98
115
|
elsif node.is_a?(Kanayago::OperatorCallNode)
|
|
@@ -121,5 +138,6 @@ class Igata
|
|
|
121
138
|
end
|
|
122
139
|
end
|
|
123
140
|
end
|
|
141
|
+
# rubocop:enable Metrics/ClassLength, Lint/DuplicateBranch
|
|
124
142
|
end
|
|
125
143
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
class Igata
|
|
4
4
|
module Extractors
|
|
5
|
+
# rubocop:disable Lint/DuplicateBranch
|
|
5
6
|
class ComparisonAnalyzer
|
|
6
7
|
COMPARISON_OPERATORS = %i[>= <= > < == !=].freeze
|
|
7
8
|
|
|
@@ -55,6 +56,22 @@ class Igata
|
|
|
55
56
|
elsif node.is_a?(Kanayago::CaseNode)
|
|
56
57
|
traverse_node(node.body, comparisons) if node.respond_to?(:body)
|
|
57
58
|
traverse_node(node.else, comparisons) if node.respond_to?(:else)
|
|
59
|
+
# Handle RescueNode (container node for rescue clauses)
|
|
60
|
+
elsif node.is_a?(Kanayago::RescueNode)
|
|
61
|
+
# Traverse head (main body before rescue)
|
|
62
|
+
if node.respond_to?(:head) && node.head
|
|
63
|
+
if node.head.is_a?(Array)
|
|
64
|
+
node.head.each { |child| traverse_node(child, comparisons) }
|
|
65
|
+
else
|
|
66
|
+
traverse_node(node.head, comparisons)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
traverse_node(node.resq, comparisons) if node.respond_to?(:resq)
|
|
70
|
+
traverse_node(node.else, comparisons) if node.respond_to?(:else)
|
|
71
|
+
# Handle RescueBodyNode
|
|
72
|
+
elsif node.is_a?(Kanayago::RescueBodyNode)
|
|
73
|
+
traverse_node(node.body, comparisons) if node.respond_to?(:body)
|
|
74
|
+
traverse_node(node.next, comparisons) if node.respond_to?(:next)
|
|
58
75
|
# Handle ScopeNode (container node)
|
|
59
76
|
elsif node.is_a?(Kanayago::ScopeNode)
|
|
60
77
|
traverse_node(node.body, comparisons) if node.respond_to?(:body)
|
|
@@ -78,7 +95,7 @@ class Igata
|
|
|
78
95
|
elsif node.is_a?(Kanayago::IntegerNode)
|
|
79
96
|
node.val.to_s
|
|
80
97
|
elsif node.is_a?(Kanayago::StringNode)
|
|
81
|
-
"\"#{node.
|
|
98
|
+
"\"#{node.ptr}\""
|
|
82
99
|
elsif node.is_a?(Kanayago::SymbolNode)
|
|
83
100
|
":#{node.ptr}"
|
|
84
101
|
elsif node.respond_to?(:mid)
|
|
@@ -97,5 +114,6 @@ class Igata
|
|
|
97
114
|
"#{left} #{operator} #{right}"
|
|
98
115
|
end
|
|
99
116
|
end
|
|
117
|
+
# rubocop:enable Lint/DuplicateBranch
|
|
100
118
|
end
|
|
101
119
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
class Igata
|
|
4
4
|
module Extractors
|
|
5
|
+
# rubocop:disable Metrics/ClassLength
|
|
5
6
|
class ConstantPath
|
|
6
7
|
def self.extract(ast)
|
|
7
8
|
new(ast).extract
|
|
@@ -9,15 +10,28 @@ class Igata
|
|
|
9
10
|
|
|
10
11
|
def initialize(ast)
|
|
11
12
|
@ast = ast
|
|
13
|
+
# If ast.body is BlockNode, find the ClassNode inside it
|
|
14
|
+
@class_node = find_class_node(ast.body)
|
|
12
15
|
end
|
|
13
16
|
|
|
14
|
-
def
|
|
17
|
+
def find_class_node(node)
|
|
18
|
+
if node.is_a?(Kanayago::ClassNode) || node.is_a?(Kanayago::ModuleNode)
|
|
19
|
+
node
|
|
20
|
+
elsif node.is_a?(Kanayago::BlockNode) && node.respond_to?(:find)
|
|
21
|
+
node.find { |n| n.is_a?(Kanayago::ClassNode) || n.is_a?(Kanayago::ModuleNode) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def extract # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
26
|
+
return nil unless @class_node
|
|
27
|
+
return nil unless @class_node.respond_to?(:cpath) && @class_node.cpath
|
|
28
|
+
|
|
15
29
|
if compact_nested? && nested?
|
|
16
30
|
# Mixed pattern: class App::User; class Profile; end; end
|
|
17
31
|
# Inner class may also be compact nested: class App::Model; class User::Profile; end
|
|
18
32
|
# May be deeply nested: class App::Model; class Admin::User; class Profile; end; end; end
|
|
19
33
|
compact_path = extract_compact_nested_path
|
|
20
|
-
nested_path = build_nested_path(@
|
|
34
|
+
nested_path = build_nested_path(@class_node)
|
|
21
35
|
path = "#{compact_path}::#{nested_path}"
|
|
22
36
|
|
|
23
37
|
Values::ConstantPath.new(
|
|
@@ -37,8 +51,8 @@ class Igata
|
|
|
37
51
|
# Regular nested pattern: class User; class Profile; end; end
|
|
38
52
|
# Inner class may also be compact nested: class User; class App::Profile; end
|
|
39
53
|
# May be deeply nested: class User; class Admin::User; class Profile; end; end; end
|
|
40
|
-
namespace = @
|
|
41
|
-
nested_path = build_nested_path(@
|
|
54
|
+
namespace = @class_node.cpath.mid.to_s
|
|
55
|
+
nested_path = build_nested_path(@class_node)
|
|
42
56
|
path = "#{namespace}::#{nested_path}"
|
|
43
57
|
|
|
44
58
|
Values::ConstantPath.new(
|
|
@@ -49,7 +63,7 @@ class Igata
|
|
|
49
63
|
else
|
|
50
64
|
# Simple pattern: class User
|
|
51
65
|
Values::ConstantPath.new(
|
|
52
|
-
path: @
|
|
66
|
+
path: @class_node.cpath.mid.to_s,
|
|
53
67
|
nested: false,
|
|
54
68
|
compact: false
|
|
55
69
|
)
|
|
@@ -60,12 +74,14 @@ class Igata
|
|
|
60
74
|
|
|
61
75
|
def compact_nested?
|
|
62
76
|
# For compact nested pattern (class User::Profile), cpath is Colon2Node with non-nil head
|
|
63
|
-
@
|
|
77
|
+
return false unless @class_node.respond_to?(:cpath)
|
|
78
|
+
|
|
79
|
+
@class_node.cpath.is_a?(Kanayago::Colon2Node) && !@class_node.cpath.head.nil?
|
|
64
80
|
end
|
|
65
81
|
|
|
66
82
|
def extract_compact_nested_path
|
|
67
83
|
# Recursively traverse cpath to build complete path
|
|
68
|
-
build_constant_path(@
|
|
84
|
+
build_constant_path(@class_node.cpath)
|
|
69
85
|
end
|
|
70
86
|
|
|
71
87
|
def build_constant_path(node) # rubocop:disable Metrics/MethodLength
|
|
@@ -87,7 +103,10 @@ class Igata
|
|
|
87
103
|
end
|
|
88
104
|
|
|
89
105
|
def nested?
|
|
90
|
-
|
|
106
|
+
return false unless @class_node.respond_to?(:body)
|
|
107
|
+
return false unless @class_node.body.respond_to?(:body)
|
|
108
|
+
|
|
109
|
+
class_body = @class_node.body.body
|
|
91
110
|
# For empty classes, class_body is BeginNode which doesn't have any?
|
|
92
111
|
return false unless class_body.respond_to?(:any?)
|
|
93
112
|
|
|
@@ -123,7 +142,7 @@ class Igata
|
|
|
123
142
|
|
|
124
143
|
def find_nested_constant_node
|
|
125
144
|
# Recursively find the deepest (innermost) class/module definition
|
|
126
|
-
find_deepest_nested_constant_node(@
|
|
145
|
+
find_deepest_nested_constant_node(@class_node)
|
|
127
146
|
end
|
|
128
147
|
|
|
129
148
|
def find_deepest_nested_constant_node(parent_node)
|
|
@@ -138,5 +157,6 @@ class Igata
|
|
|
138
157
|
deeper_child || direct_child
|
|
139
158
|
end
|
|
140
159
|
end
|
|
160
|
+
# rubocop:enable Metrics/ClassLength
|
|
141
161
|
end
|
|
142
162
|
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Igata
|
|
4
|
+
module Extractors
|
|
5
|
+
# rubocop:disable Metrics/ClassLength, Lint/DuplicateBranch
|
|
6
|
+
class ExceptionAnalyzer
|
|
7
|
+
def self.extract(method_node)
|
|
8
|
+
new(method_node).extract
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(method_node)
|
|
12
|
+
@method_node = method_node
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def extract
|
|
16
|
+
return [] unless @method_node
|
|
17
|
+
return [] unless @method_node.respond_to?(:defn)
|
|
18
|
+
|
|
19
|
+
exceptions = []
|
|
20
|
+
# DefinitionNode has a defn field that contains ScopeNode
|
|
21
|
+
defn_node = @method_node.defn
|
|
22
|
+
traverse_node(defn_node, exceptions) if defn_node
|
|
23
|
+
exceptions
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def traverse_node(node, exceptions) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
29
|
+
return unless node
|
|
30
|
+
|
|
31
|
+
# Handle raise statement (FunctionCallNode with mid == :raise)
|
|
32
|
+
if node.is_a?(Kanayago::FunctionCallNode) && node.respond_to?(:mid) && node.mid.to_s == "raise"
|
|
33
|
+
exceptions << extract_raise_info(node)
|
|
34
|
+
# Handle rescue clause
|
|
35
|
+
elsif node.is_a?(Kanayago::RescueNode)
|
|
36
|
+
extract_rescue_info(node, exceptions)
|
|
37
|
+
# Continue traversing head (main body before rescue), rescue clause, and else
|
|
38
|
+
if node.respond_to?(:head) && node.head
|
|
39
|
+
if node.head.is_a?(Array)
|
|
40
|
+
node.head.each { |child| traverse_node(child, exceptions) }
|
|
41
|
+
else
|
|
42
|
+
traverse_node(node.head, exceptions)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
traverse_node(node.resq, exceptions) if node.respond_to?(:resq)
|
|
46
|
+
traverse_node(node.else, exceptions) if node.respond_to?(:else)
|
|
47
|
+
# Handle RescueBody node (contains exception class list)
|
|
48
|
+
elsif node.is_a?(Kanayago::RescueBodyNode)
|
|
49
|
+
extract_rescue_body_info(node, exceptions)
|
|
50
|
+
traverse_node(node.body, exceptions) if node.respond_to?(:body)
|
|
51
|
+
traverse_node(node.next, exceptions) if node.respond_to?(:next)
|
|
52
|
+
# Handle IfStatementNode (raise might be inside if)
|
|
53
|
+
elsif node.is_a?(Kanayago::IfStatementNode)
|
|
54
|
+
traverse_node(node.body, exceptions) if node.respond_to?(:body)
|
|
55
|
+
traverse_node(node.elsif, exceptions) if node.respond_to?(:elsif)
|
|
56
|
+
traverse_node(node.else, exceptions) if node.respond_to?(:else)
|
|
57
|
+
# Handle UnlessStatementNode
|
|
58
|
+
elsif node.is_a?(Kanayago::UnlessStatementNode)
|
|
59
|
+
traverse_node(node.body, exceptions) if node.respond_to?(:body)
|
|
60
|
+
traverse_node(node.else, exceptions) if node.respond_to?(:else)
|
|
61
|
+
# Handle CaseNode
|
|
62
|
+
elsif node.is_a?(Kanayago::CaseNode)
|
|
63
|
+
traverse_node(node.body, exceptions) if node.respond_to?(:body)
|
|
64
|
+
traverse_node(node.else, exceptions) if node.respond_to?(:else)
|
|
65
|
+
# Handle ScopeNode (container node)
|
|
66
|
+
elsif node.is_a?(Kanayago::ScopeNode)
|
|
67
|
+
traverse_node(node.body, exceptions) if node.respond_to?(:body)
|
|
68
|
+
# Handle BlockNode (container for multiple statements)
|
|
69
|
+
elsif node.is_a?(Kanayago::BlockNode)
|
|
70
|
+
node.each { |child| traverse_node(child, exceptions) } if node.respond_to?(:each)
|
|
71
|
+
# Handle other container nodes
|
|
72
|
+
elsif node.respond_to?(:body) && node.body.respond_to?(:each)
|
|
73
|
+
node.body.each { |child| traverse_node(child, exceptions) }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockNesting
|
|
78
|
+
def extract_raise_info(call_node)
|
|
79
|
+
# Extract exception class and message from raise statement
|
|
80
|
+
# raise ArgumentError, "message" => args is ListNode with val array
|
|
81
|
+
exception_class = "StandardError" # default
|
|
82
|
+
message = nil
|
|
83
|
+
|
|
84
|
+
if call_node.respond_to?(:args) && call_node.args
|
|
85
|
+
args_node = call_node.args
|
|
86
|
+
|
|
87
|
+
# ListNode contains exception class and message in val array
|
|
88
|
+
if args_node.is_a?(Kanayago::ListNode) && args_node.respond_to?(:val)
|
|
89
|
+
args_array = args_node.val
|
|
90
|
+
|
|
91
|
+
if args_array.is_a?(Array)
|
|
92
|
+
# First argument is exception class (if constant) or message (if string)
|
|
93
|
+
first_arg = args_array[0]
|
|
94
|
+
if first_arg
|
|
95
|
+
if first_arg.is_a?(Kanayago::ConstantNode)
|
|
96
|
+
exception_class = first_arg.vid.to_s
|
|
97
|
+
elsif first_arg.is_a?(Kanayago::StringNode)
|
|
98
|
+
# raise "message" - just a string message (StandardError)
|
|
99
|
+
message = first_arg.ptr
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Second argument is message (if exists)
|
|
104
|
+
second_arg = args_array[1]
|
|
105
|
+
message = second_arg.ptr if second_arg.is_a?(Kanayago::StringNode)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
context = build_raise_context(exception_class, message)
|
|
111
|
+
Values::ExceptionInfo.new(
|
|
112
|
+
type: :raise,
|
|
113
|
+
exception_class: exception_class,
|
|
114
|
+
message: message,
|
|
115
|
+
context: context
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockNesting
|
|
119
|
+
|
|
120
|
+
def extract_rescue_info(rescue_node, exceptions)
|
|
121
|
+
# RescueNode might contain multiple rescue bodies
|
|
122
|
+
# We'll handle this via RescueBodyNode traversal
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
126
|
+
def extract_rescue_body_info(rescue_body_node, exceptions)
|
|
127
|
+
# Extract exception class from rescue clause
|
|
128
|
+
# rescue ArgumentError => e
|
|
129
|
+
# rescue ArgumentError, StandardError => e
|
|
130
|
+
exception_classes = []
|
|
131
|
+
|
|
132
|
+
if rescue_body_node.respond_to?(:args) && rescue_body_node.args
|
|
133
|
+
args_node = rescue_body_node.args
|
|
134
|
+
|
|
135
|
+
# args is ListNode with val array containing exception classes
|
|
136
|
+
if args_node.is_a?(Kanayago::ListNode) && args_node.respond_to?(:val)
|
|
137
|
+
args_array = args_node.val
|
|
138
|
+
|
|
139
|
+
if args_array.is_a?(Array)
|
|
140
|
+
args_array.each do |arg|
|
|
141
|
+
exception_classes << arg.vid.to_s if arg.is_a?(Kanayago::ConstantNode)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# If no exceptions specified, it catches StandardError
|
|
148
|
+
exception_classes = ["StandardError"] if exception_classes.empty?
|
|
149
|
+
|
|
150
|
+
exception_classes.each do |exc_class|
|
|
151
|
+
context = "rescue #{exc_class}"
|
|
152
|
+
exceptions << Values::ExceptionInfo.new(
|
|
153
|
+
type: :rescue,
|
|
154
|
+
exception_class: exc_class,
|
|
155
|
+
message: nil,
|
|
156
|
+
context: context
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
161
|
+
|
|
162
|
+
def build_raise_context(exception_class, message)
|
|
163
|
+
if message
|
|
164
|
+
"raise #{exception_class}, \"#{message}\""
|
|
165
|
+
else
|
|
166
|
+
"raise #{exception_class}"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
# rubocop:enable Metrics/ClassLength, Lint/DuplicateBranch
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -11,6 +11,7 @@ class Igata
|
|
|
11
11
|
@class_node = class_node
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# rubocop:disable Metrics/MethodLength
|
|
14
15
|
def extract
|
|
15
16
|
class_body = @class_node.body.body
|
|
16
17
|
# For empty classes, class_body is BeginNode which doesn't have filter_map
|
|
@@ -25,9 +26,22 @@ class Igata
|
|
|
25
26
|
# Extract comparison information for each method
|
|
26
27
|
comparisons = ComparisonAnalyzer.extract(node)
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
# Extract exception information for each method
|
|
30
|
+
exceptions = ExceptionAnalyzer.extract(node)
|
|
31
|
+
|
|
32
|
+
# Extract boundary value suggestions for each method
|
|
33
|
+
boundary_values = BoundaryValueGenerator.extract(comparisons)
|
|
34
|
+
|
|
35
|
+
Values::MethodInfo.new(
|
|
36
|
+
name: node.mid.to_s,
|
|
37
|
+
branches: branches,
|
|
38
|
+
comparisons: comparisons,
|
|
39
|
+
exceptions: exceptions,
|
|
40
|
+
boundary_values: boundary_values
|
|
41
|
+
)
|
|
29
42
|
end
|
|
30
43
|
end
|
|
44
|
+
# rubocop:enable Metrics/MethodLength
|
|
31
45
|
end
|
|
32
46
|
end
|
|
33
47
|
end
|