igata 0.1.0 → 0.2.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/.ruby-version +1 -0
- data/CHANGELOG.md +91 -0
- data/QUICKSTART.md +168 -0
- data/README.md +78 -0
- data/exe/igata +44 -0
- data/lib/igata/error.rb +5 -0
- data/lib/igata/extractors/branch_analyzer.rb +125 -0
- data/lib/igata/extractors/comparison_analyzer.rb +101 -0
- data/lib/igata/extractors/constant_path.rb +142 -0
- data/lib/igata/extractors/method_names.rb +33 -0
- data/lib/igata/formatters/base.rb +35 -0
- data/lib/igata/formatters/minitest.rb +32 -0
- data/lib/igata/formatters/rspec.rb +32 -0
- data/lib/igata/formatters/templates/minitest/class.erb +12 -0
- data/lib/igata/formatters/templates/minitest/method.erb +9 -0
- data/lib/igata/formatters/templates/rspec/class.erb +12 -0
- data/lib/igata/formatters/templates/rspec/method.erb +11 -0
- data/lib/igata/values.rb +33 -0
- data/lib/igata/version.rb +2 -2
- data/lib/igata.rb +68 -3
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78199aa96fa495664cb461c46bbabdce3afa9a21f3083e5f49e41cb47e66f909
|
|
4
|
+
data.tar.gz: e2a0d35b071882c6b28ea13d1b88f846e3bb74adcf1ca92d7700aa7be635ee44
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c5b75401c77c515dfdcb5968d44c7c4e8569ed10ebc03c5e7a7a8ab7e8225aa086cabcb66c99c7519f3c35fc5fb9f2789409d56d5d4961995c07ae79192df94
|
|
7
|
+
data.tar.gz: eb80ea990597cda4110a8b7e431d41d6c19c8af8bf5387e2e8ad270ea273e1b39f913107069ec8da2c8b836dbbe1efe8fe885aae9fc07afb26a53568efb49f26
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.4.7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,96 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2025-10-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Introduced Formatter architecture for supporting multiple test frameworks:
|
|
8
|
+
- `Formatters::Base`: Abstract base class for all formatters
|
|
9
|
+
- `Formatters::Minitest`: Minitest-specific formatter implementation
|
|
10
|
+
- `Formatters::RSpec`: RSpec-specific formatter implementation
|
|
11
|
+
- `MethodNotOverriddenError`: Custom error for abstract method violations
|
|
12
|
+
- Separated `Igata::Error` into dedicated file
|
|
13
|
+
- Added CLI formatter option:
|
|
14
|
+
- `-f, --formatter FORMATTER`: Specify test framework formatter (minitest or rspec)
|
|
15
|
+
- Added RSpec support:
|
|
16
|
+
- RSpec formatter generates `describe` and `it` blocks
|
|
17
|
+
- RSpec templates (class.erb and method.erb) for RSpec-style test generation
|
|
18
|
+
- Supports all branch and comparison analysis features in RSpec format
|
|
19
|
+
- Branch and comparison comments in RSpec tests (e.g., `# Branches: if (value > 0)`)
|
|
20
|
+
- Unit tests for RSpec formatter (6 tests)
|
|
21
|
+
- Integration tests for RSpec formatter (3 tests)
|
|
22
|
+
- Added branch analysis functionality:
|
|
23
|
+
- `BranchInfo` value object: Stores branch information (type, condition)
|
|
24
|
+
- `BranchAnalyzer` extractor: Detects if/unless/case statements in methods
|
|
25
|
+
- `MethodInfo` now includes `branches` field for branch information
|
|
26
|
+
- Minitest template generates branch comments with condition information (e.g., `# Branches: if (age >= 18), unless (user.valid?)`)
|
|
27
|
+
- Extracts condition expressions for if/unless/case statements
|
|
28
|
+
- Handles method calls, comparisons, and variable references in conditions
|
|
29
|
+
- Added comparison analysis functionality:
|
|
30
|
+
- `ComparisonInfo` value object: Stores comparison information (operator, left, right, context)
|
|
31
|
+
- `ComparisonAnalyzer` extractor: Detects comparison operators (>=, <=, >, <, ==, !=) in methods
|
|
32
|
+
- `MethodInfo` now includes `comparisons` field for comparison information
|
|
33
|
+
- Minitest template generates comparison comments (e.g., `# Comparisons: >= (age >= 18)`)
|
|
34
|
+
- Supports multiple comparisons with AND/OR operators (e.g., `value >= 0 && value <= 150`)
|
|
35
|
+
- Detects comparisons in various contexts: direct returns, if/unless conditions, nested expressions
|
|
36
|
+
- Handles different expression types: local variables, instance variables, integers, strings, symbols
|
|
37
|
+
- Added comprehensive unit tests:
|
|
38
|
+
- Extractor tests: `ConstantPath` (10 tests), `MethodNames` (6 tests), `BranchAnalyzer` (6 tests), `ComparisonAnalyzer` (6 tests)
|
|
39
|
+
- Formatter tests: `Base` (3 tests), `Minitest` (6 tests)
|
|
40
|
+
- Total: 37 new unit tests added
|
|
41
|
+
- Added integration tests for branch analysis:
|
|
42
|
+
- `class_with_branches` test fixture with if/unless/case statements
|
|
43
|
+
- 4 integration tests verifying branch comment generation
|
|
44
|
+
- Added integration tests for comparison analysis:
|
|
45
|
+
- `class_with_comparisons` test fixture with various comparison operators
|
|
46
|
+
- 6 integration tests verifying comparison comment generation
|
|
47
|
+
- Added ERB templates (class.erb and method.erb) to generate Minitest test code skeleton
|
|
48
|
+
- Implemented test generation supporting various class/module nesting patterns:
|
|
49
|
+
- Simple classes: `class User`
|
|
50
|
+
- Regular nested classes: `class User; class Profile; end; end`
|
|
51
|
+
- Compact nested classes: `class User::Profile`
|
|
52
|
+
- Deep nested classes: `class App::User::Profile` (supports any depth)
|
|
53
|
+
- Mixed nested patterns: `class App::User; class Profile; end; end`
|
|
54
|
+
- Double compact nested: `class App::Model; class User::Profile; end; end`
|
|
55
|
+
- Triple nested with mixed patterns: `class App::Model; class Admin::User; class Profile; end; end; end`
|
|
56
|
+
- Compact nested modules: `module User::Updator`
|
|
57
|
+
- Generated test files include properly indented method definitions with skip statements
|
|
58
|
+
- Test methods are separated by blank lines for better readability
|
|
59
|
+
- Implemented CLI with OptionParser support
|
|
60
|
+
- `-o, --output FILE`: Write output to file instead of stdout
|
|
61
|
+
- `-h, --help`: Display help message
|
|
62
|
+
- `-v, --version`: Display version information
|
|
63
|
+
- File argument support: `igata file.rb`
|
|
64
|
+
- Stdin support: `cat file.rb | igata`
|
|
65
|
+
- Added CLI regression tests (test/cli_test.rb) covering all command-line options and input methods
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- Reorganized project structure for multi-formatter support:
|
|
70
|
+
- Moved templates to `lib/igata/formatters/templates/minitest/`
|
|
71
|
+
- Reorganized fixtures to `test/fixtures/integration/minitest/`
|
|
72
|
+
- Test type (integration/unit) is now the primary hierarchy, formatter (minitest) is secondary
|
|
73
|
+
- Separated tests into `test/integration/` and `test/unit/`
|
|
74
|
+
- Refactored code generation to use Formatter pattern:
|
|
75
|
+
- `Igata` class delegates to formatter for code generation
|
|
76
|
+
- Accepts `formatter:` parameter (default: `:minitest`)
|
|
77
|
+
- Supports custom formatter classes
|
|
78
|
+
- Refactored AST analysis logic into Extractor pattern for better separation of concerns
|
|
79
|
+
- Introduced Value Objects (`ConstantPath`, `MethodInfo`, `BranchInfo`) using `Data.define` for immutable data structures
|
|
80
|
+
- Implemented recursive traversal for deeply nested class/module definitions
|
|
81
|
+
- AST extraction is now handled by specialized extractors:
|
|
82
|
+
- `Extractors::ConstantPath`: Extracts full constant paths from nested structures
|
|
83
|
+
- `Extractors::MethodNames`: Extracts method information from class nodes
|
|
84
|
+
- `Extractors::BranchAnalyzer`: Analyzes control flow branches in methods
|
|
85
|
+
- `Extractors::ComparisonAnalyzer`: Analyzes comparison operators in methods
|
|
86
|
+
- `MethodNames` extractor now automatically runs `BranchAnalyzer` and `ComparisonAnalyzer` for each method
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
|
|
90
|
+
- Fixed empty class handling in Extractors:
|
|
91
|
+
- `ConstantPath` now handles BeginNode (empty classes) correctly
|
|
92
|
+
- `MethodNames` returns empty array for BeginNode instead of raising error
|
|
93
|
+
|
|
3
94
|
## [0.1.0] - 2025-10-25
|
|
4
95
|
|
|
5
96
|
- Initial release
|
data/QUICKSTART.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Quick Start Guide
|
|
2
|
+
|
|
3
|
+
Get started with Igata in 5 minutes.
|
|
4
|
+
|
|
5
|
+
## What is Igata?
|
|
6
|
+
|
|
7
|
+
Igata (鋳型 = mold/template) generates test code templates from your Ruby classes. It analyzes your code structure and creates test skeletons with helpful comments about branches and comparisons, so you can focus on writing test logic instead of boilerplate.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
gem install igata
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or add to your Gemfile:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle add igata
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Your First Test Generation
|
|
22
|
+
|
|
23
|
+
### 1. Create a sample Ruby class
|
|
24
|
+
|
|
25
|
+
Create a file `user.rb`:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
class User
|
|
29
|
+
def initialize(name, age)
|
|
30
|
+
@name = name
|
|
31
|
+
@age = age
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def adult?
|
|
35
|
+
@age >= 18
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def greeting
|
|
39
|
+
if @name
|
|
40
|
+
"Hello, #{@name}!"
|
|
41
|
+
else
|
|
42
|
+
"Hello, Guest!"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Generate Minitest (default)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
igata user.rb > test/test_user.rb
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Output:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# frozen_string_literal: true
|
|
58
|
+
|
|
59
|
+
require "test_helper"
|
|
60
|
+
|
|
61
|
+
class UserTest < Minitest::Test
|
|
62
|
+
def test_initialize
|
|
63
|
+
skip "Not implemented yet"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_adult?
|
|
67
|
+
# Comparisons: >= (@age >= 18)
|
|
68
|
+
skip "Not implemented yet"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_greeting
|
|
72
|
+
# Branches: if (@name)
|
|
73
|
+
skip "Not implemented yet"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Generate RSpec
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
igata user.rb -f rspec > spec/user_spec.rb
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Output:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# frozen_string_literal: true
|
|
88
|
+
|
|
89
|
+
require "spec_helper"
|
|
90
|
+
|
|
91
|
+
RSpec.describe User do
|
|
92
|
+
describe "#initialize" do
|
|
93
|
+
it "works correctly" do
|
|
94
|
+
pending "Not implemented yet"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe "#adult?" do
|
|
99
|
+
# Comparisons: >= (@age >= 18)
|
|
100
|
+
it "works correctly" do
|
|
101
|
+
pending "Not implemented yet"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe "#greeting" do
|
|
106
|
+
# Branches: if (@name)
|
|
107
|
+
it "works correctly" do
|
|
108
|
+
pending "Not implemented yet"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Common Usage Patterns
|
|
115
|
+
|
|
116
|
+
### Generate from stdin
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
cat lib/user.rb | igata
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Specify output file
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
igata lib/user.rb -o test/test_user.rb
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Multiple formatters
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Minitest (default)
|
|
132
|
+
igata lib/user.rb > test/test_user.rb
|
|
133
|
+
|
|
134
|
+
# RSpec
|
|
135
|
+
igata lib/user.rb -f rspec > spec/user_spec.rb
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Understanding the Output
|
|
139
|
+
|
|
140
|
+
Igata adds helpful comments to guide your test implementation:
|
|
141
|
+
|
|
142
|
+
- **`# Branches:`** - Shows conditional logic (if/unless/case) with conditions
|
|
143
|
+
- **`# Comparisons:`** - Shows comparison operators (>=, <=, >, <, ==, !=) suggesting boundary value tests
|
|
144
|
+
|
|
145
|
+
These comments help you:
|
|
146
|
+
1. Identify edge cases to test
|
|
147
|
+
2. Write comprehensive test coverage
|
|
148
|
+
3. Understand method complexity at a glance
|
|
149
|
+
4. **Assist LLMs** - GitHub Copilot, Claude, ChatGPT, and other LLMs may use these comments as context to generate more appropriate test code
|
|
150
|
+
|
|
151
|
+
## Next Steps
|
|
152
|
+
|
|
153
|
+
- See [README.md](README.md) for complete documentation
|
|
154
|
+
- Check [CHANGELOG.md](CHANGELOG.md) for feature details
|
|
155
|
+
- Write your test logic inside the generated skeleton
|
|
156
|
+
- Remove `skip` / `pending` statements as you implement tests
|
|
157
|
+
|
|
158
|
+
## Tips
|
|
159
|
+
|
|
160
|
+
1. **Nested classes work**: `class User::Profile` generates proper test structure
|
|
161
|
+
2. **Run tests first**: Generated tests use `skip`/`pending` so they won't fail
|
|
162
|
+
3. **Combine with TDD**: Generate template → Write test logic → Implement feature
|
|
163
|
+
4. **Use comments**: Branch and comparison comments guide your test cases
|
|
164
|
+
|
|
165
|
+
## Need Help?
|
|
166
|
+
|
|
167
|
+
- [GitHub Issues](https://github.com/S-H-GAMELINKS/igata/issues)
|
|
168
|
+
- [Full Documentation](README.md)
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Generate test code from AST node produces by Ruby's Parser
|
|
4
4
|
|
|
5
|
+
**[📚 Quick Start Guide](QUICKSTART.md)** | [Full Documentation](#usage) | [Changelog](CHANGELOG.md)
|
|
6
|
+
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
Install the gem and add to the application's Gemfile by executing:
|
|
@@ -18,10 +20,86 @@ gem install igata
|
|
|
18
20
|
|
|
19
21
|
## Usage
|
|
20
22
|
|
|
23
|
+
### Basic Usage
|
|
24
|
+
|
|
25
|
+
Generate Minitest tests (default):
|
|
26
|
+
|
|
21
27
|
```bash
|
|
22
28
|
bundle exec igata lib/user.rb > test/test_user.rb
|
|
23
29
|
```
|
|
24
30
|
|
|
31
|
+
Generate RSpec tests:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bundle exec igata lib/user.rb -f rspec > spec/user_spec.rb
|
|
35
|
+
# or
|
|
36
|
+
bundle exec igata lib/user.rb --formatter rspec > spec/user_spec.rb
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Supported Formatters
|
|
40
|
+
|
|
41
|
+
- `minitest` (default): Generates Minitest-style tests
|
|
42
|
+
- `rspec`: Generates RSpec-style tests
|
|
43
|
+
|
|
44
|
+
### Example
|
|
45
|
+
|
|
46
|
+
Given a Ruby class:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
class User
|
|
50
|
+
def initialize(name, age)
|
|
51
|
+
@name = name
|
|
52
|
+
@age = age
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def adult?
|
|
56
|
+
@age >= 18
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Minitest output:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# frozen_string_literal: true
|
|
65
|
+
|
|
66
|
+
require "test_helper"
|
|
67
|
+
|
|
68
|
+
class UserTest < Minitest::Test
|
|
69
|
+
def test_initialize
|
|
70
|
+
skip "Not implemented yet"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_adult?
|
|
74
|
+
# Comparisons: >= (@age >= 18)
|
|
75
|
+
skip "Not implemented yet"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
RSpec output:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# frozen_string_literal: true
|
|
84
|
+
|
|
85
|
+
require "spec_helper"
|
|
86
|
+
|
|
87
|
+
RSpec.describe User do
|
|
88
|
+
describe "#initialize" do
|
|
89
|
+
it "works correctly" do
|
|
90
|
+
pending "Not implemented yet"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe "#adult?" do
|
|
95
|
+
# Comparisons: >= (@age >= 18)
|
|
96
|
+
it "works correctly" do
|
|
97
|
+
pending "Not implemented yet"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
25
103
|
## Development
|
|
26
104
|
|
|
27
105
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/exe/igata
CHANGED
|
@@ -2,3 +2,47 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "igata"
|
|
5
|
+
require "optparse"
|
|
6
|
+
|
|
7
|
+
options = { formatter: :minitest }
|
|
8
|
+
OptionParser.new do |opts|
|
|
9
|
+
opts.banner = "Usage: igata [options] [file]"
|
|
10
|
+
opts.version = Igata::VERSION
|
|
11
|
+
|
|
12
|
+
opts.on("-f", "--formatter FORMATTER", %i[minitest rspec],
|
|
13
|
+
"Test framework formatter (minitest, rspec)") do |formatter|
|
|
14
|
+
options[:formatter] = formatter
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
opts.on("-o", "--output FILE", "Write output to FILE instead of stdout") do |file|
|
|
18
|
+
options[:output] = file
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
22
|
+
puts opts
|
|
23
|
+
exit
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
opts.on("-v", "--version", "Show version") do
|
|
27
|
+
puts Igata::VERSION
|
|
28
|
+
exit
|
|
29
|
+
end
|
|
30
|
+
end.parse!
|
|
31
|
+
|
|
32
|
+
# Read input from file or stdin
|
|
33
|
+
source = if ARGV.empty?
|
|
34
|
+
$stdin.read
|
|
35
|
+
else
|
|
36
|
+
File.read(ARGV[0])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Generate test code
|
|
40
|
+
igata = Igata.new(source, formatter: options[:formatter])
|
|
41
|
+
output = igata.generate
|
|
42
|
+
|
|
43
|
+
# Write output to file or stdout
|
|
44
|
+
if options[:output]
|
|
45
|
+
File.write(options[:output], output)
|
|
46
|
+
else
|
|
47
|
+
puts output
|
|
48
|
+
end
|
data/lib/igata/error.rb
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Igata
|
|
4
|
+
module Extractors
|
|
5
|
+
class BranchAnalyzer
|
|
6
|
+
def self.extract(method_node)
|
|
7
|
+
new(method_node).extract
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(method_node)
|
|
11
|
+
@method_node = method_node
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def extract
|
|
15
|
+
return [] unless @method_node
|
|
16
|
+
return [] unless @method_node.respond_to?(:defn)
|
|
17
|
+
|
|
18
|
+
branches = []
|
|
19
|
+
# DefinitionNode has a defn field that contains ScopeNode
|
|
20
|
+
defn_node = @method_node.defn
|
|
21
|
+
traverse_node(defn_node, branches) if defn_node
|
|
22
|
+
branches
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def traverse_node(node, branches) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
28
|
+
return unless node
|
|
29
|
+
|
|
30
|
+
# Handle IfStatementNode (if/elsif/else)
|
|
31
|
+
if node.is_a?(Kanayago::IfStatementNode)
|
|
32
|
+
branches << Values::BranchInfo.new(
|
|
33
|
+
type: :if,
|
|
34
|
+
condition: extract_condition(node)
|
|
35
|
+
)
|
|
36
|
+
# Continue traversing child nodes
|
|
37
|
+
traverse_node(node.body, branches) if node.respond_to?(:body)
|
|
38
|
+
traverse_node(node.elsif, branches) if node.respond_to?(:elsif)
|
|
39
|
+
traverse_node(node.else, branches) if node.respond_to?(:else)
|
|
40
|
+
# Handle UnlessStatementNode
|
|
41
|
+
elsif node.is_a?(Kanayago::UnlessStatementNode)
|
|
42
|
+
branches << Values::BranchInfo.new(
|
|
43
|
+
type: :unless,
|
|
44
|
+
condition: extract_condition(node)
|
|
45
|
+
)
|
|
46
|
+
traverse_node(node.body, branches) if node.respond_to?(:body)
|
|
47
|
+
traverse_node(node.else, branches) if node.respond_to?(:else)
|
|
48
|
+
# Handle CaseNode (case/when)
|
|
49
|
+
elsif node.is_a?(Kanayago::CaseNode)
|
|
50
|
+
branches << Values::BranchInfo.new(
|
|
51
|
+
type: :case,
|
|
52
|
+
condition: extract_condition(node)
|
|
53
|
+
)
|
|
54
|
+
# Traverse when branches
|
|
55
|
+
traverse_node(node.body, branches) if node.respond_to?(:body)
|
|
56
|
+
traverse_node(node.else, branches) if node.respond_to?(:else)
|
|
57
|
+
# Handle ScopeNode (container node)
|
|
58
|
+
elsif node.is_a?(Kanayago::ScopeNode)
|
|
59
|
+
traverse_node(node.body, branches) if node.respond_to?(:body)
|
|
60
|
+
# Handle BlockNode (container for multiple statements)
|
|
61
|
+
elsif node.is_a?(Kanayago::BlockNode)
|
|
62
|
+
node.each { |child| traverse_node(child, branches) } if node.respond_to?(:each)
|
|
63
|
+
# Handle other container nodes
|
|
64
|
+
elsif node.respond_to?(:body) && node.body.respond_to?(:each)
|
|
65
|
+
node.body.each { |child| traverse_node(child, branches) }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def extract_condition(branch_node) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
70
|
+
return nil unless branch_node
|
|
71
|
+
|
|
72
|
+
# Extract condition based on branch type
|
|
73
|
+
condition_node = if branch_node.is_a?(Kanayago::IfStatementNode) || branch_node.is_a?(Kanayago::UnlessStatementNode)
|
|
74
|
+
branch_node.cond if branch_node.respond_to?(:cond)
|
|
75
|
+
elsif branch_node.is_a?(Kanayago::CaseNode)
|
|
76
|
+
branch_node.head if branch_node.respond_to?(:head)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
return nil unless condition_node
|
|
80
|
+
|
|
81
|
+
extract_expression(condition_node)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def extract_expression(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
85
|
+
return "" unless node
|
|
86
|
+
|
|
87
|
+
# Handle different node types for source reconstruction
|
|
88
|
+
if node.is_a?(Kanayago::LocalVariableNode)
|
|
89
|
+
node.vid.to_s
|
|
90
|
+
elsif node.is_a?(Kanayago::InstanceVariableNode)
|
|
91
|
+
node.vid.to_s
|
|
92
|
+
elsif node.is_a?(Kanayago::IntegerNode)
|
|
93
|
+
node.val.to_s
|
|
94
|
+
elsif node.is_a?(Kanayago::StringNode)
|
|
95
|
+
"\"#{node.val}\""
|
|
96
|
+
elsif node.is_a?(Kanayago::SymbolNode)
|
|
97
|
+
":#{node.ptr}"
|
|
98
|
+
elsif node.is_a?(Kanayago::OperatorCallNode)
|
|
99
|
+
# Handle comparison operators like age >= 18
|
|
100
|
+
left = extract_expression(node.recv)
|
|
101
|
+
operator = node.mid.to_s
|
|
102
|
+
right = extract_expression(node.args&.val&.first)
|
|
103
|
+
"#{left} #{operator} #{right}"
|
|
104
|
+
elsif node.is_a?(Kanayago::CallNode)
|
|
105
|
+
# Handle method calls like user.valid?
|
|
106
|
+
if node.respond_to?(:recv) && node.recv
|
|
107
|
+
receiver = extract_expression(node.recv)
|
|
108
|
+
method_name = node.mid.to_s
|
|
109
|
+
"#{receiver}.#{method_name}"
|
|
110
|
+
elsif node.respond_to?(:mid)
|
|
111
|
+
node.mid.to_s
|
|
112
|
+
else
|
|
113
|
+
"call"
|
|
114
|
+
end
|
|
115
|
+
elsif node.respond_to?(:mid)
|
|
116
|
+
node.mid.to_s
|
|
117
|
+
elsif node.is_a?(Integer) || node.is_a?(String)
|
|
118
|
+
node.to_s
|
|
119
|
+
else
|
|
120
|
+
node.class.name.split("::").last.gsub("Node", "").downcase
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Igata
|
|
4
|
+
module Extractors
|
|
5
|
+
class ComparisonAnalyzer
|
|
6
|
+
COMPARISON_OPERATORS = %i[>= <= > < == !=].freeze
|
|
7
|
+
|
|
8
|
+
def self.extract(method_node)
|
|
9
|
+
new(method_node).extract
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(method_node)
|
|
13
|
+
@method_node = method_node
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def extract
|
|
17
|
+
return [] unless @method_node
|
|
18
|
+
return [] unless @method_node.respond_to?(:defn)
|
|
19
|
+
|
|
20
|
+
comparisons = []
|
|
21
|
+
# DefinitionNode has a defn field that contains ScopeNode
|
|
22
|
+
defn_node = @method_node.defn
|
|
23
|
+
traverse_node(defn_node, comparisons) if defn_node
|
|
24
|
+
comparisons
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def traverse_node(node, comparisons) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
30
|
+
return unless node
|
|
31
|
+
|
|
32
|
+
# Handle OperatorCallNode (comparison operators are OperatorCallNode in Kanayago)
|
|
33
|
+
if node.is_a?(Kanayago::OperatorCallNode) && COMPARISON_OPERATORS.include?(node.mid)
|
|
34
|
+
comparisons << Values::ComparisonInfo.new(
|
|
35
|
+
operator: node.mid,
|
|
36
|
+
left: extract_expression(node.recv),
|
|
37
|
+
right: extract_expression(node.args&.val&.first),
|
|
38
|
+
context: build_context(node)
|
|
39
|
+
)
|
|
40
|
+
# Handle AndNode and OrNode (traverse both sides)
|
|
41
|
+
elsif node.is_a?(Kanayago::AndNode) || node.is_a?(Kanayago::OrNode)
|
|
42
|
+
traverse_node(node.first, comparisons) if node.respond_to?(:first)
|
|
43
|
+
traverse_node(node.second, comparisons) if node.respond_to?(:second)
|
|
44
|
+
# Handle IfStatementNode (traverse condition and body)
|
|
45
|
+
elsif node.is_a?(Kanayago::IfStatementNode)
|
|
46
|
+
traverse_node(node.cond, comparisons) if node.respond_to?(:cond)
|
|
47
|
+
traverse_node(node.body, comparisons) if node.respond_to?(:body)
|
|
48
|
+
traverse_node(node.else, comparisons) if node.respond_to?(:else)
|
|
49
|
+
# Handle UnlessStatementNode
|
|
50
|
+
elsif node.is_a?(Kanayago::UnlessStatementNode)
|
|
51
|
+
traverse_node(node.cond, comparisons) if node.respond_to?(:cond)
|
|
52
|
+
traverse_node(node.body, comparisons) if node.respond_to?(:body)
|
|
53
|
+
traverse_node(node.else, comparisons) if node.respond_to?(:else)
|
|
54
|
+
# Handle CaseNode
|
|
55
|
+
elsif node.is_a?(Kanayago::CaseNode)
|
|
56
|
+
traverse_node(node.body, comparisons) if node.respond_to?(:body)
|
|
57
|
+
traverse_node(node.else, comparisons) if node.respond_to?(:else)
|
|
58
|
+
# Handle ScopeNode (container node)
|
|
59
|
+
elsif node.is_a?(Kanayago::ScopeNode)
|
|
60
|
+
traverse_node(node.body, comparisons) if node.respond_to?(:body)
|
|
61
|
+
# Handle BlockNode (container for multiple statements)
|
|
62
|
+
elsif node.is_a?(Kanayago::BlockNode)
|
|
63
|
+
node.each { |child| traverse_node(child, comparisons) } if node.respond_to?(:each)
|
|
64
|
+
# Handle other container nodes
|
|
65
|
+
elsif node.respond_to?(:body) && node.body.respond_to?(:each)
|
|
66
|
+
node.body.each { |child| traverse_node(child, comparisons) }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def extract_expression(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
71
|
+
return "" unless node
|
|
72
|
+
|
|
73
|
+
# Handle different node types for source reconstruction
|
|
74
|
+
if node.is_a?(Kanayago::LocalVariableNode)
|
|
75
|
+
node.vid.to_s
|
|
76
|
+
elsif node.is_a?(Kanayago::InstanceVariableNode)
|
|
77
|
+
node.vid.to_s
|
|
78
|
+
elsif node.is_a?(Kanayago::IntegerNode)
|
|
79
|
+
node.val.to_s
|
|
80
|
+
elsif node.is_a?(Kanayago::StringNode)
|
|
81
|
+
"\"#{node.val}\""
|
|
82
|
+
elsif node.is_a?(Kanayago::SymbolNode)
|
|
83
|
+
":#{node.ptr}"
|
|
84
|
+
elsif node.respond_to?(:mid)
|
|
85
|
+
node.mid.to_s
|
|
86
|
+
elsif node.is_a?(Integer) || node.is_a?(String)
|
|
87
|
+
node.to_s
|
|
88
|
+
else
|
|
89
|
+
node.class.name.split("::").last.gsub("Node", "").downcase
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_context(call_node)
|
|
94
|
+
left = extract_expression(call_node.recv)
|
|
95
|
+
operator = call_node.mid.to_s
|
|
96
|
+
right = extract_expression(call_node.args&.val&.first)
|
|
97
|
+
"#{left} #{operator} #{right}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Igata
|
|
4
|
+
module Extractors
|
|
5
|
+
class ConstantPath
|
|
6
|
+
def self.extract(ast)
|
|
7
|
+
new(ast).extract
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(ast)
|
|
11
|
+
@ast = ast
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def extract # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
15
|
+
if compact_nested? && nested?
|
|
16
|
+
# Mixed pattern: class App::User; class Profile; end; end
|
|
17
|
+
# Inner class may also be compact nested: class App::Model; class User::Profile; end
|
|
18
|
+
# May be deeply nested: class App::Model; class Admin::User; class Profile; end; end; end
|
|
19
|
+
compact_path = extract_compact_nested_path
|
|
20
|
+
nested_path = build_nested_path(@ast.body)
|
|
21
|
+
path = "#{compact_path}::#{nested_path}"
|
|
22
|
+
|
|
23
|
+
Values::ConstantPath.new(
|
|
24
|
+
path: path,
|
|
25
|
+
nested: true, # Has nested class inside
|
|
26
|
+
compact: true # Outer is compact nested
|
|
27
|
+
)
|
|
28
|
+
elsif compact_nested?
|
|
29
|
+
# Compact nested pattern (no inner nesting): class User::Profile
|
|
30
|
+
path = extract_compact_nested_path
|
|
31
|
+
Values::ConstantPath.new(
|
|
32
|
+
path: path,
|
|
33
|
+
nested: false, # No nested class inside
|
|
34
|
+
compact: true
|
|
35
|
+
)
|
|
36
|
+
elsif nested?
|
|
37
|
+
# Regular nested pattern: class User; class Profile; end; end
|
|
38
|
+
# Inner class may also be compact nested: class User; class App::Profile; end
|
|
39
|
+
# May be deeply nested: class User; class Admin::User; class Profile; end; end; end
|
|
40
|
+
namespace = @ast.body.cpath.mid.to_s
|
|
41
|
+
nested_path = build_nested_path(@ast.body)
|
|
42
|
+
path = "#{namespace}::#{nested_path}"
|
|
43
|
+
|
|
44
|
+
Values::ConstantPath.new(
|
|
45
|
+
path: path,
|
|
46
|
+
nested: true, # Has nested class inside
|
|
47
|
+
compact: false
|
|
48
|
+
)
|
|
49
|
+
else
|
|
50
|
+
# Simple pattern: class User
|
|
51
|
+
Values::ConstantPath.new(
|
|
52
|
+
path: @ast.body.cpath.mid.to_s,
|
|
53
|
+
nested: false,
|
|
54
|
+
compact: false
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def compact_nested?
|
|
62
|
+
# For compact nested pattern (class User::Profile), cpath is Colon2Node with non-nil head
|
|
63
|
+
@ast.body.cpath.is_a?(Kanayago::Colon2Node) && !@ast.body.cpath.head.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def extract_compact_nested_path
|
|
67
|
+
# Recursively traverse cpath to build complete path
|
|
68
|
+
build_constant_path(@ast.body.cpath)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_constant_path(node) # rubocop:disable Metrics/MethodLength
|
|
72
|
+
case node
|
|
73
|
+
when Kanayago::Colon2Node
|
|
74
|
+
if node.head.nil?
|
|
75
|
+
# For "class User", head is nil so return only mid
|
|
76
|
+
node.mid.to_s
|
|
77
|
+
else
|
|
78
|
+
# For "User::Profile", combine head(User) + mid(Profile)
|
|
79
|
+
"#{build_constant_path(node.head)}::#{node.mid}"
|
|
80
|
+
end
|
|
81
|
+
when Kanayago::ConstantNode
|
|
82
|
+
# Top-level constant name
|
|
83
|
+
node.vid.to_s
|
|
84
|
+
else
|
|
85
|
+
node.to_s
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def nested?
|
|
90
|
+
class_body = @ast.body.body.body
|
|
91
|
+
# For empty classes, class_body is BeginNode which doesn't have any?
|
|
92
|
+
return false unless class_body.respond_to?(:any?)
|
|
93
|
+
|
|
94
|
+
class_body.any? { |node| constant_definition_node?(node) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def constant_definition_node?(node)
|
|
98
|
+
node.is_a?(Kanayago::ClassNode) || node.is_a?(Kanayago::ModuleNode)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def build_nested_path(parent_node) # rubocop:disable Metrics/MethodLength
|
|
102
|
+
# Find direct child class/module under the parent node
|
|
103
|
+
class_body = parent_node.body.body
|
|
104
|
+
# For empty classes, class_body is BeginNode which doesn't have find
|
|
105
|
+
return nil unless class_body.respond_to?(:find)
|
|
106
|
+
|
|
107
|
+
child_node = class_body.find { |node| constant_definition_node?(node) }
|
|
108
|
+
return nil unless child_node
|
|
109
|
+
|
|
110
|
+
# Get child class name (considering compact nesting)
|
|
111
|
+
child_class_path = build_constant_path(child_node.cpath)
|
|
112
|
+
|
|
113
|
+
# Check if there's deeper nesting
|
|
114
|
+
deeper_path = build_nested_path(child_node)
|
|
115
|
+
|
|
116
|
+
# Concatenate if deeper nesting exists, otherwise return current path
|
|
117
|
+
if deeper_path
|
|
118
|
+
"#{child_class_path}::#{deeper_path}"
|
|
119
|
+
else
|
|
120
|
+
child_class_path
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def find_nested_constant_node
|
|
125
|
+
# Recursively find the deepest (innermost) class/module definition
|
|
126
|
+
find_deepest_nested_constant_node(@ast.body)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def find_deepest_nested_constant_node(parent_node)
|
|
130
|
+
# Find direct child class/module under the current node
|
|
131
|
+
direct_child = parent_node.body.body.find { |node| constant_definition_node?(node) }
|
|
132
|
+
return nil unless direct_child
|
|
133
|
+
|
|
134
|
+
# Check if there's deeper nesting in the child node
|
|
135
|
+
deeper_child = find_deepest_nested_constant_node(direct_child)
|
|
136
|
+
|
|
137
|
+
# Return deeper nesting if exists, otherwise return current child
|
|
138
|
+
deeper_child || direct_child
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Igata
|
|
4
|
+
module Extractors
|
|
5
|
+
class MethodNames
|
|
6
|
+
def self.extract(class_node)
|
|
7
|
+
new(class_node).extract
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(class_node)
|
|
11
|
+
@class_node = class_node
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def extract
|
|
15
|
+
class_body = @class_node.body.body
|
|
16
|
+
# For empty classes, class_body is BeginNode which doesn't have filter_map
|
|
17
|
+
return [] unless class_body.respond_to?(:filter_map)
|
|
18
|
+
|
|
19
|
+
class_body.filter_map do |node|
|
|
20
|
+
next unless node.is_a?(Kanayago::DefinitionNode)
|
|
21
|
+
|
|
22
|
+
# Extract branch information for each method
|
|
23
|
+
branches = BranchAnalyzer.extract(node)
|
|
24
|
+
|
|
25
|
+
# Extract comparison information for each method
|
|
26
|
+
comparisons = ComparisonAnalyzer.extract(node)
|
|
27
|
+
|
|
28
|
+
Values::MethodInfo.new(name: node.mid.to_s, branches: branches, comparisons: comparisons)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../error"
|
|
4
|
+
|
|
5
|
+
class Igata
|
|
6
|
+
module Formatters
|
|
7
|
+
class MethodNotOverriddenError < Igata::Error; end
|
|
8
|
+
|
|
9
|
+
class Base
|
|
10
|
+
def initialize(constant_info, method_infos)
|
|
11
|
+
@constant_info = constant_info
|
|
12
|
+
@method_infos = method_infos
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def generate
|
|
16
|
+
raise MethodNotOverriddenError, "#{self.class}#generate must be implemented"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def template_path(name)
|
|
22
|
+
File.join(templates_dir, "#{name}.erb")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def templates_dir
|
|
26
|
+
raise MethodNotOverriddenError, "#{self.class}#templates_dir must be implemented"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def render_template(template_file, binding_context)
|
|
30
|
+
template_content = File.read(template_file)
|
|
31
|
+
ERB.new(template_content, trim_mode: "<>").result(binding_context)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Igata
|
|
6
|
+
module Formatters
|
|
7
|
+
class Minitest < Base
|
|
8
|
+
def generate
|
|
9
|
+
class_name = @constant_info.path
|
|
10
|
+
methods = generate_methods
|
|
11
|
+
|
|
12
|
+
template = ERB.new(File.read(template_path("class")), trim_mode: "<>")
|
|
13
|
+
template.result(binding)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def templates_dir
|
|
19
|
+
File.join(__dir__, "templates", "minitest")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate_methods
|
|
23
|
+
@method_infos.map do |method_info|
|
|
24
|
+
method_name = method_info.name
|
|
25
|
+
branches = method_info.branches
|
|
26
|
+
comparisons = method_info.comparisons
|
|
27
|
+
ERB.new(File.read(template_path("method")), trim_mode: "<>").result(binding)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Igata
|
|
6
|
+
module Formatters
|
|
7
|
+
class RSpec < Base
|
|
8
|
+
def generate
|
|
9
|
+
class_name = @constant_info.path
|
|
10
|
+
methods = generate_methods
|
|
11
|
+
|
|
12
|
+
template = ERB.new(File.read(template_path("class")), trim_mode: "<>")
|
|
13
|
+
template.result(binding)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def templates_dir
|
|
19
|
+
File.join(__dir__, "templates", "rspec")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate_methods
|
|
23
|
+
@method_infos.map do |method_info|
|
|
24
|
+
method_name = method_info.name
|
|
25
|
+
branches = method_info.branches
|
|
26
|
+
comparisons = method_info.comparisons
|
|
27
|
+
ERB.new(File.read(template_path("method")), trim_mode: "<>").result(binding)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
def test_<%= method_name %>
|
|
2
|
+
<% if branches && !branches.empty? %>
|
|
3
|
+
# Branches: <%= branches.map { |b| b.condition ? "#{b.type} (#{b.condition})" : b.type.to_s }.join(", ") %>
|
|
4
|
+
<% end %>
|
|
5
|
+
<% if comparisons && !comparisons.empty? %>
|
|
6
|
+
# Comparisons: <%= comparisons.map { |c| "#{c.operator} (#{c.context})" }.join(", ") %>
|
|
7
|
+
<% end %>
|
|
8
|
+
skip "Not implemented yet"
|
|
9
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
describe "#<%= method_name %>" do
|
|
2
|
+
<% if branches && !branches.empty? %>
|
|
3
|
+
# Branches: <%= branches.map { |b| b.condition ? "#{b.type} (#{b.condition})" : b.type.to_s }.join(", ") %>
|
|
4
|
+
<% end %>
|
|
5
|
+
<% if comparisons && !comparisons.empty? %>
|
|
6
|
+
# Comparisons: <%= comparisons.map { |c| "#{c.operator} (#{c.context})" }.join(", ") %>
|
|
7
|
+
<% end %>
|
|
8
|
+
it "works correctly" do
|
|
9
|
+
pending "Not implemented yet"
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/igata/values.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Igata
|
|
4
|
+
module Values
|
|
5
|
+
ConstantPath = Data.define(
|
|
6
|
+
:path, # "User::Profile"
|
|
7
|
+
:nested, # true/false
|
|
8
|
+
:compact # true/false (compact nested like "class User::Profile")
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
MethodInfo = Data.define(
|
|
12
|
+
:name, # "initialize"
|
|
13
|
+
:branches, # Array of BranchInfo (default: [])
|
|
14
|
+
:comparisons # Array of ComparisonInfo (default: [])
|
|
15
|
+
) do
|
|
16
|
+
def initialize(name:, branches: [], comparisons: [])
|
|
17
|
+
super(name: name, branches: branches, comparisons: comparisons)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
BranchInfo = Data.define(
|
|
22
|
+
:type, # :if, :unless, :case, :ternary
|
|
23
|
+
:condition # condition expression as string
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
ComparisonInfo = Data.define(
|
|
27
|
+
:operator, # :>=, :<=, :>, :<, :==, :!=
|
|
28
|
+
:left, # left side expression
|
|
29
|
+
:right, # right side expression
|
|
30
|
+
:context # full expression as string (e.g., "age >= 18")
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/igata/version.rb
CHANGED
data/lib/igata.rb
CHANGED
|
@@ -1,8 +1,73 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "kanayago"
|
|
4
|
+
require "erb"
|
|
5
|
+
|
|
3
6
|
require_relative "igata/version"
|
|
7
|
+
require_relative "igata/error"
|
|
8
|
+
require_relative "igata/values"
|
|
9
|
+
require_relative "igata/extractors/constant_path"
|
|
10
|
+
require_relative "igata/extractors/method_names"
|
|
11
|
+
require_relative "igata/extractors/branch_analyzer"
|
|
12
|
+
require_relative "igata/extractors/comparison_analyzer"
|
|
13
|
+
require_relative "igata/formatters/minitest"
|
|
14
|
+
require_relative "igata/formatters/rspec"
|
|
15
|
+
|
|
16
|
+
class Igata
|
|
17
|
+
def initialize(source, formatter: :minitest)
|
|
18
|
+
@source = source
|
|
19
|
+
@ast = Kanayago.parse(source)
|
|
20
|
+
@formatter = formatter
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def generate
|
|
24
|
+
constant_info = Extractors::ConstantPath.extract(@ast)
|
|
25
|
+
target_node = find_target_class_node(constant_info)
|
|
26
|
+
method_infos = Extractors::MethodNames.extract(target_node)
|
|
27
|
+
|
|
28
|
+
formatter_class = resolve_formatter(@formatter)
|
|
29
|
+
formatter_class.new(constant_info, method_infos).generate
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def resolve_formatter(formatter)
|
|
35
|
+
case formatter
|
|
36
|
+
when :minitest
|
|
37
|
+
Formatters::Minitest
|
|
38
|
+
when :rspec
|
|
39
|
+
Formatters::RSpec
|
|
40
|
+
when Class
|
|
41
|
+
formatter
|
|
42
|
+
else
|
|
43
|
+
raise Error, "Unknown formatter: #{formatter}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def find_target_class_node(constant_info)
|
|
48
|
+
if constant_info.nested
|
|
49
|
+
# When nested is true, recursively find the deepest (innermost) class
|
|
50
|
+
# - class User; class Profile; end; end
|
|
51
|
+
# - class App::User; class Profile; end; end (mixed pattern)
|
|
52
|
+
# - class App::Model; class Admin::User; class Profile; end; end; end (3+ levels)
|
|
53
|
+
find_deepest_class_node(@ast.body)
|
|
54
|
+
else
|
|
55
|
+
# When nested is false, @ast.body itself is the target
|
|
56
|
+
# - class User
|
|
57
|
+
# - class User::Profile
|
|
58
|
+
@ast.body
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def find_deepest_class_node(parent_node)
|
|
63
|
+
# Find direct child class/module under the current node
|
|
64
|
+
direct_child = parent_node.body.body.find { |node| node.is_a?(Kanayago::ClassNode) || node.is_a?(Kanayago::ModuleNode) }
|
|
65
|
+
return nil unless direct_child
|
|
66
|
+
|
|
67
|
+
# Check if there's deeper nesting in the child node
|
|
68
|
+
deeper_child = find_deepest_class_node(direct_child)
|
|
4
69
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
70
|
+
# Return deeper nesting if exists, otherwise return current child
|
|
71
|
+
deeper_child || direct_child
|
|
72
|
+
end
|
|
8
73
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: igata
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- S-H-GAMELINKS
|
|
@@ -17,12 +17,27 @@ executables:
|
|
|
17
17
|
extensions: []
|
|
18
18
|
extra_rdoc_files: []
|
|
19
19
|
files:
|
|
20
|
+
- ".ruby-version"
|
|
20
21
|
- CHANGELOG.md
|
|
21
22
|
- LICENSE.txt
|
|
23
|
+
- QUICKSTART.md
|
|
22
24
|
- README.md
|
|
23
25
|
- Rakefile
|
|
24
26
|
- exe/igata
|
|
25
27
|
- lib/igata.rb
|
|
28
|
+
- lib/igata/error.rb
|
|
29
|
+
- lib/igata/extractors/branch_analyzer.rb
|
|
30
|
+
- lib/igata/extractors/comparison_analyzer.rb
|
|
31
|
+
- lib/igata/extractors/constant_path.rb
|
|
32
|
+
- lib/igata/extractors/method_names.rb
|
|
33
|
+
- lib/igata/formatters/base.rb
|
|
34
|
+
- lib/igata/formatters/minitest.rb
|
|
35
|
+
- lib/igata/formatters/rspec.rb
|
|
36
|
+
- lib/igata/formatters/templates/minitest/class.erb
|
|
37
|
+
- lib/igata/formatters/templates/minitest/method.erb
|
|
38
|
+
- lib/igata/formatters/templates/rspec/class.erb
|
|
39
|
+
- lib/igata/formatters/templates/rspec/method.erb
|
|
40
|
+
- lib/igata/values.rb
|
|
26
41
|
- lib/igata/version.rb
|
|
27
42
|
- sig/igata.rbs
|
|
28
43
|
homepage: https://github.com/S-H-GAMELINKS/igata
|
|
@@ -32,6 +47,7 @@ metadata:
|
|
|
32
47
|
homepage_uri: https://github.com/S-H-GAMELINKS/igata
|
|
33
48
|
source_code_uri: https://github.com/S-H-GAMELINKS/igata
|
|
34
49
|
changelog_uri: https://github.com/S-H-GAMELINKS/igata/blob/main/CHANGELOG.md
|
|
50
|
+
rubygems_mfa_required: 'true'
|
|
35
51
|
rdoc_options: []
|
|
36
52
|
require_paths:
|
|
37
53
|
- lib
|
|
@@ -46,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
46
62
|
- !ruby/object:Gem::Version
|
|
47
63
|
version: '0'
|
|
48
64
|
requirements: []
|
|
49
|
-
rubygems_version: 3.
|
|
65
|
+
rubygems_version: 3.6.9
|
|
50
66
|
specification_version: 4
|
|
51
67
|
summary: Generate test code from AST node produces by Ruby's Parser
|
|
52
68
|
test_files: []
|