bullematic 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +180 -0
- data/Rakefile +26 -0
- data/Steepfile +15 -0
- data/assets/logo-header.svg +28 -0
- data/lib/bullematic/ast/finder.rb +175 -0
- data/lib/bullematic/ast/parser.rb +58 -0
- data/lib/bullematic/ast/rewriter.rb +153 -0
- data/lib/bullematic/configuration.rb +66 -0
- data/lib/bullematic/detection.rb +109 -0
- data/lib/bullematic/fixer.rb +110 -0
- data/lib/bullematic/integrations/minitest.rb +37 -0
- data/lib/bullematic/integrations/rails.rb +40 -0
- data/lib/bullematic/integrations/rspec.rb +33 -0
- data/lib/bullematic/logger.rb +103 -0
- data/lib/bullematic/notifier.rb +76 -0
- data/lib/bullematic/version.rb +6 -0
- data/lib/bullematic.rb +54 -0
- data/rbs_collection.lock.yaml +24 -0
- data/rbs_collection.yaml +14 -0
- data/sig/generated/bullematic/ast/finder.rbs +91 -0
- data/sig/generated/bullematic/ast/parser.rbs +39 -0
- data/sig/generated/bullematic/ast/rewriter.rbs +77 -0
- data/sig/generated/bullematic/configuration.rbs +135 -0
- data/sig/generated/bullematic/detection.rbs +117 -0
- data/sig/generated/bullematic/fixer.rbs +33 -0
- data/sig/generated/bullematic/integrations/minitest.rbs +18 -0
- data/sig/generated/bullematic/integrations/rails.rbs +20 -0
- data/sig/generated/bullematic/integrations/rspec.rbs +10 -0
- data/sig/generated/bullematic/logger.rbs +61 -0
- data/sig/generated/bullematic/notifier.rbs +30 -0
- data/sig/generated/bullematic/version.rbs +5 -0
- data/sig/generated/bullematic.rbs +27 -0
- data/sig/stubs/rails.rbs +87 -0
- metadata +108 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5e3046689e0ebe235c6c512c31f7ad7c6252dadb773a576f81bbad8d2e6010e7
|
|
4
|
+
data.tar.gz: 10dff920eed6301bf55440c13530ad424cf64f29fa25344da634d7bd4ffe58df
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8bc920365f940b1fdd4f5953d7b3c62a5bd2ca5094c99b22ec77122b69d44b8051985b003a0d9aa0b2963d1ba2ce13d88b324187c1aaf04d5170d9effe81b324
|
|
7
|
+
data.tar.gz: 04ba28c1ea4ab1f335b5711e93b33cb6826b62240dc4b19039a654cb21099f0e449fb5ddf08c4443a40c364659c5accb33df47d11663748092f5d1651c43a496
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-01-13
|
|
11
|
+
|
|
12
|
+
- Initial release
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo-header.svg" alt="Bullematic header logo">
|
|
3
|
+
<span>Auto-fix N+1 queries detected by Bullet at runtime</span>
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://badge.fury.io/rb/bullematic"><img src="https://badge.fury.io/rb/bullematic.svg" alt="Gem Version"></a>
|
|
8
|
+
<a href="https://github.com/ydah/bullematic/actions/workflows/main.yml"><img src="https://github.com/ydah/bullematic/actions/workflows/main.yml/badge.svg" alt="CI"></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="#features">Features</a> ·
|
|
13
|
+
<a href="#installation">Installation</a> ·
|
|
14
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
15
|
+
<a href="#configuration">Configuration</a> ·
|
|
16
|
+
<a href="#how-it-works">How It Works</a> ·
|
|
17
|
+
<a href="#integrations">Integrations</a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
Bullematic hooks into [Bullet](https://github.com/flyerhzm/bullet) notifications, captures N+1 detections, and rewrites your Ruby code with `includes`, `preload`, or `eager_load` using [Prism](https://github.com/ruby/prism) AST parsing. It is designed to run in development and test environments where Bullet already runs.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- Automatic N+1 fixes for Rails scopes and queries
|
|
27
|
+
- Runtime capture via Bullet notifications
|
|
28
|
+
- AST-based rewrites powered by Prism
|
|
29
|
+
- Dry-run mode and optional backups
|
|
30
|
+
- RSpec, Minitest, and Rails integrations
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Add to your Gemfile:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
gem 'bullematic', group: [:development, :test]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then install:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bundle install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- Ruby 3.1+
|
|
49
|
+
- Bullet 6.0+
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
1. Configure Bullematic in your Rails initializer or test setup:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# config/initializers/bullematic.rb (for Rails)
|
|
57
|
+
# or in your test setup
|
|
58
|
+
|
|
59
|
+
Bullematic.configure do |config|
|
|
60
|
+
config.enabled = true
|
|
61
|
+
config.auto_fix = true
|
|
62
|
+
config.target_paths = %w[app/controllers app/models app/services]
|
|
63
|
+
config.skip_paths = %w[app/controllers/admin]
|
|
64
|
+
config.dry_run = false # Set to true to preview changes without applying
|
|
65
|
+
config.fix_strategy = :includes # :includes, :preload, or :eager_load
|
|
66
|
+
config.backup = true # Create backup files before modifying
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
2. Enable Bullematic at runtime:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
BULLEMATIC=1 bundle exec rspec
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Integrations
|
|
77
|
+
|
|
78
|
+
### RSpec
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# spec/spec_helper.rb or spec/rails_helper.rb
|
|
82
|
+
require 'bullematic/integrations/rspec'
|
|
83
|
+
|
|
84
|
+
RSpec.configure do |config|
|
|
85
|
+
# ... your existing config
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Bullematic::Integrations::RSpec.setup
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Minitest
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# test/test_helper.rb
|
|
95
|
+
require 'bullematic/integrations/minitest'
|
|
96
|
+
|
|
97
|
+
Bullematic::Integrations::Minitest.setup
|
|
98
|
+
|
|
99
|
+
class ActiveSupport::TestCase
|
|
100
|
+
include Bullematic::Integrations::Minitest::TestHelper
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Rails
|
|
105
|
+
|
|
106
|
+
Bullematic auto-loads via Railtie in development and test environments when the gem is loaded.
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
| Option | Type | Default | Description |
|
|
111
|
+
|--------|------|---------|-------------|
|
|
112
|
+
| `enabled` | Boolean | `true` | Enable or disable Bullematic |
|
|
113
|
+
| `auto_fix` | Boolean | `true` | Automatically apply fixes |
|
|
114
|
+
| `target_paths` | Array | `['app/controllers', 'app/models', 'app/services']` | Paths to consider for fixes |
|
|
115
|
+
| `skip_paths` | Array | `[]` | Paths to skip |
|
|
116
|
+
| `dry_run` | Boolean | `false` | Preview changes without applying |
|
|
117
|
+
| `backup` | Boolean | `false` | Create `.bullematic.bak` backup files |
|
|
118
|
+
| `fix_strategy` | Symbol | `:includes` | Strategy: `:includes`, `:preload`, or `:eager_load` |
|
|
119
|
+
| `logger` | Logger | Auto-configured | Custom logger instance |
|
|
120
|
+
| `debug` | Boolean | `false` | Enable debug mode (raises errors) |
|
|
121
|
+
|
|
122
|
+
## How It Works
|
|
123
|
+
|
|
124
|
+
1. Bullet detects an N+1 query during request or test execution
|
|
125
|
+
2. Bullematic captures the notification and stack trace
|
|
126
|
+
3. At the end of the run, Bullematic parses the file with Prism
|
|
127
|
+
4. The AST finder locates the query that triggered the N+1
|
|
128
|
+
5. The AST rewriter inserts the appropriate `includes` call
|
|
129
|
+
6. The file is written back (or logged in dry-run mode)
|
|
130
|
+
|
|
131
|
+
### Example Transformation
|
|
132
|
+
|
|
133
|
+
Before:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
class PostsController < ApplicationController
|
|
137
|
+
def index
|
|
138
|
+
@posts = Post.all # N+1 when accessing @posts.each { |p| p.comments }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
After:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
class PostsController < ApplicationController
|
|
147
|
+
def index
|
|
148
|
+
@posts = Post.includes(:comments).all
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Limitations
|
|
154
|
+
|
|
155
|
+
- Dynamic queries built with `send` or metaprogramming may not be fixable
|
|
156
|
+
- Complex conditional branches can be hard to rewrite
|
|
157
|
+
- Already-optimized queries are skipped
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
git clone https://github.com/ydah/bullematic.git
|
|
163
|
+
cd bullematic
|
|
164
|
+
bin/setup
|
|
165
|
+
bundle exec rake spec
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Contributing
|
|
169
|
+
|
|
170
|
+
Bug reports and pull requests are welcome at https://github.com/ydah/bullematic.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
Released under the [MIT License](https://opensource.org/licenses/MIT).
|
|
175
|
+
|
|
176
|
+
## Acknowledgements
|
|
177
|
+
|
|
178
|
+
- [Bullet](https://github.com/flyerhzm/bullet) - N+1 query detection
|
|
179
|
+
- [bulletmark_repairer](https://github.com/makicamel/bulletmark_repairer) - Similar concept, inspiration for this gem
|
|
180
|
+
- [Prism](https://github.com/ruby/prism) - Ruby parser used for AST manipulation
|
data/Rakefile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
desc "Generate RBS files from inline annotations"
|
|
9
|
+
task :rbs_inline do
|
|
10
|
+
sh "bundle exec rbs-inline --output sig lib"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
desc "Run Steep type check"
|
|
14
|
+
task :steep do
|
|
15
|
+
sh "bundle exec steep check"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "Install RBS collection"
|
|
19
|
+
task :rbs_collection_install do
|
|
20
|
+
sh "bundle exec rbs collection install"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "Run all type checks (generate RBS and run Steep)"
|
|
24
|
+
task typecheck: %i[rbs_inline steep]
|
|
25
|
+
|
|
26
|
+
task default: %i[spec]
|
data/Steepfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
D = Steep::Diagnostic
|
|
4
|
+
|
|
5
|
+
target :lib do
|
|
6
|
+
signature "sig/generated"
|
|
7
|
+
signature "sig/stubs"
|
|
8
|
+
|
|
9
|
+
check "lib"
|
|
10
|
+
|
|
11
|
+
configure_code_diagnostics do |hash|
|
|
12
|
+
hash[D::Ruby::UnannotatedEmptyCollection] = :hint
|
|
13
|
+
hash[D::Ruby::BlockTypeMismatch] = :hint
|
|
14
|
+
end
|
|
15
|
+
end
|