callback_hell 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1e3c885812a73a0a4fd89036e2d1897b64a467130924de668bdfb7dd4c90091b
4
+ data.tar.gz: 55d44b2e7296574ec3657b9e4d44ea4dfc5b68b9510f87853ff33e41fc40ad9c
5
+ SHA512:
6
+ metadata.gz: a43af18de970ceab56f70a67cbe08c7348b3ff69a5acdbeba4dbdb395173bcfe96c40bc9a1f441074512d42a142e58183c43bcb415c875fdbb93d3e7753b15b7
7
+ data.tar.gz: 7f51fd27e54b2ee70cf3927d716f3d5c95bfb1d14f240deadd0806ccd9200e480644240c24db0ea045dfb735786519ef844c83794955de24d2c78ae03f3f9bdf
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Change log
2
+
3
+ ## 0.2.0
4
+
5
+ - Initial public release. ([@yaroslav][])
6
+
7
+ - Make the default namespace-level rake task to run the full report (callbacks and validations). ([@yaroslav][])
8
+
9
+ ## 0.1.1
10
+
11
+ - Migrated from `terminal-table` to `table_tennis`. ([@palkan][])
12
+
13
+ - Added `mode=default | full` parameter to hide Rails internal callbacks/validations by default. ([@palkan][])
14
+
15
+ ## 0.1.0
16
+
17
+ - Initial version. See `README.md` for details. ([@yaroslav][])
18
+
19
+ [@palkan]: https://github.com/palkan
20
+ [@yaroslav]: https://github.com/yaroslav
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2024 Yaroslav Markin, Vladimir Dementyev
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,174 @@
1
+ [![Gem Version](https://badge.fury.io/rb/callback_hell.svg)](https://rubygems.org/gems/callback_hell) [![Build](https://github.com/evilmartians/callback_hell/workflows/Build/badge.svg)](https://github.com/evilmartians/callback_hell/actions)
2
+
3
+ # Callback Hell
4
+
5
+ > You live in it.
6
+
7
+ Callback Hell is a Ruby gem for use with Ruby on Rails applications.
8
+
9
+ It analyzes your Rails application models and provides actionable insights on callbacks and validations. Use it to identify models that might benefit from refactoring, spot callback pollution from gems and associations, and keep your models clean and maintainable.
10
+
11
+ <a href="https://evilmartians.com/?utm_source=callback_hell">
12
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
13
+ </a>
14
+
15
+ ## Why bother?
16
+
17
+ As Rails applications grow, callbacks can quickly spiral out of control. Callback Hell helps you:
18
+
19
+ - **Visualize callback complexity** across your entire application
20
+ - **Identify callback hotspots** that need refactoring attention
21
+ - **Track callback origins**: distinguish your code from Rails internals and gem callbacks
22
+ - **Spot inheritance issues** and understand callback propagation
23
+ - **Audit conditional callbacks** that might be hiding bugs
24
+
25
+ ## Quick start
26
+
27
+ Add to your Rails application's `Gemfile`:
28
+
29
+ ```ruby
30
+ gem "callback_hell", group: :development
31
+ ```
32
+
33
+ And then:
34
+
35
+ ```bash
36
+ bundle install
37
+ ```
38
+
39
+ ### Basic usage
40
+
41
+ Generate a complete analysis report (callbacks _and_ validations):
42
+
43
+ ```bash
44
+ bin/rails ch
45
+ ```
46
+
47
+ Sample output:
48
+
49
+ <img src="https://raw.githubusercontent.com/evilmartians/callback_hell/refs/heads/main/assets/report.png" width="758" height="767" alt="Callback Hell sample output">
50
+
51
+ Or run specific reports:
52
+
53
+ ```bash
54
+ # Just callbacks
55
+ bin/rails ch:callbacks
56
+
57
+ # Just validations
58
+ bin/rails ch:validations
59
+ ```
60
+
61
+ ### Requirements
62
+
63
+ We support Ruby 3.0+ and Rails 7.0+.
64
+
65
+ ## Usage
66
+
67
+ **Note:** you can use both `rake` or `bin/rails` as you wish.
68
+
69
+ ### Command line options
70
+
71
+ All rake tasks support the following options:
72
+
73
+ #### Output formats
74
+
75
+ ```bash
76
+ # Table format (default)
77
+ bin/rails callback_hell:callbacks format=table
78
+
79
+ # Line format: detailed per-callback breakdown, useful for debugging
80
+ bin/rails callback_hell:callbacks format=line
81
+
82
+ # GitHub Actions format for CI/CD
83
+ bin/rails callback_hell:callbacks format=github
84
+ ```
85
+
86
+ #### Model filtering
87
+
88
+ ```bash
89
+ # Analyze specific model by class name
90
+ bin/rails ch model=User
91
+
92
+ # Or by file path
93
+ bin/rails ch model=admin/user
94
+
95
+ # Works with namespaced models too
96
+ bin/rails ch model=Admin::User
97
+ ```
98
+
99
+ #### Sorting
100
+
101
+ ```bash
102
+ # Sort by callback count (default)
103
+ bin/rails ch sort=size:desc
104
+
105
+ # Sort alphabetically
106
+ bin/rails ch sort=name:asc
107
+ ```
108
+
109
+ #### Analysis modes
110
+
111
+ ```bash
112
+ # Default mode - your callbacks only
113
+ bin/rails ch mode=default
114
+
115
+ # Full mode - includes Rails internals and associations
116
+ bin/rails ch mode=full
117
+ ```
118
+
119
+ #### Custom model paths
120
+
121
+ ```bash
122
+ # Include models from engines or non-standard locations
123
+ bin/rails ch path=engines/admin/app/models,lib/models
124
+ ```
125
+
126
+ ### Understanding the output
127
+
128
+ #### Callback origins
129
+
130
+ - **Own**: Callbacks defined in your application code
131
+ - **Rails**: Built-in Rails framework callbacks
132
+ - **Gems**: Callbacks from external gems
133
+ - **Inherited**: Callbacks inherited from parent classes or modules
134
+
135
+ #### Special Categories
136
+
137
+ - **Conditional**: Callbacks with `:if` or `:unless` conditions
138
+ - **Associations**: Auto-generated by Rails associations (`has_many`, `belongs_to`, etc.)
139
+ - **Attributes**: Generated by Rails attribute features (encryption, normalization, etc.)
140
+
141
+ ### Integration with CI/CD
142
+
143
+ You can try the GitHub Actions format to integrate with your CI pipeline:
144
+
145
+ ```yaml
146
+ # .github/workflows/callback_analysis.yml
147
+ name: Callback Analysis
148
+ on: [push, pull_request]
149
+
150
+ jobs:
151
+ analyze:
152
+ runs-on: ubuntu-latest
153
+ steps:
154
+ - uses: actions/checkout@v3
155
+ - uses: ruby/setup-ruby@v1
156
+ with:
157
+ bundler-cache: true
158
+ - name: Analyze callbacks
159
+ run: bin/rails ch format=github
160
+ ```
161
+
162
+ ## Credits and acknowledgements
163
+
164
+ Callback Hell is a spiritual successor of the [arca](https://github.com/jonmagic/arca) gem by [Jonathan Hoyt](https://github.com/jonmagic).
165
+
166
+ The entire idea and a detailed specification for the gem was done by [Vladimir Dementyev](https://github.com/palkan) who initially offered it as a test task for [Evil Martians](https://evilmartians.com/?utm_source=callback_hell) Ruby developer take-home task.
167
+
168
+ ## Contributing
169
+
170
+ Bug reports and pull requests are welcome at [https://github.com/evilmartians/callback_hell](https://github.com/evilmartians/callback_hell).
171
+
172
+ ## License
173
+
174
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/file_id.diz ADDED
@@ -0,0 +1,19 @@
1
+ .. ..
2
+ .GP YH.
3
+ GP^Y$b.dBBBBBBBBBBBBBBBBBBBb.d$Y^YD .dBBBBBBBBBBBBBBBBBBBb.
4
+ Y$$$$$$$$$$$$$$$$$$$$$$$$$Y dSSSSSSSSSSSSSSSSSSSSSSSSSb
5
+ d$$$$$$$$$$$$$$$$$$$$$$$$$$$b dSYSSSYYSSSYYSSSYYSSSYYSSSYSb
6
+ M$$$$$$$$$$$$$$$$$$$$$$$$$$$M MS.`Y´.L`Y´.L`Y´.L`Y´.L`Y´.SM
7
+ M$$$$$$$$$$$$$$$$$$$$$$$$$$$M MSSb.dSSb.dSSb.dSSb.dSSb.dSSM
8
+ M$$$$$$P^^^Y$$$$$P^^^Y$$$$$$M MSSSSSSP^^^YSSSSSP^^^YSSSSSSM
9
+ M$$$$$´.qop.`$$$´.qop.`$$$$$M .qGSSSSSS´.qop.`SSS´.qop.`SSSSSSDp.
10
+ M$$$$$ G( )D $$$ G( )D $$$$$M GY´MSSSSS G( )D SSS G( )D SSSSSM`YD
11
+ M$$$$$.`YPP´.$$$.`YPP´.$$$$$M Gb.MSSSSS.`YPP´.SSS.`YPP´.SSSSSM.dD
12
+ M$$$$$$bwwwd$$$$$bwwwd$$$$$$M `YGSSSSSSSbwwwdSSSSSbwwwdSSSSSSSDY´
13
+ M$$$$$$$$$$$$$$$$$$$$$$$$$$$M MSSSSSSSSSSSSSSSSSSSSSSSSSSSM
14
+ M$$$$$$$$$$$$$$$$$$$$$$$$$$$M MSSSSSSSSSSSSSSSSSSSSSSSSSSSM
15
+ MBBBBBBBBBBBBBBBBBBBBBBBBBBBM MBBBBBBBBBBBBBBBBBBBBBBBBBBBM
16
+
17
+ twitter.com/evilmartians github.com/evilmartians jobs: evl.ms/jobs
18
+
19
+ since 2006
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CallbackHell
4
+ module Analyzers
5
+ class CallbackAnalyzer
6
+ RAILS_GEMS = %w[
7
+ actioncable actionmailbox actionmailer actionpack actiontext
8
+ actionview activejob activemodel activerecord activestorage
9
+ activesupport railties
10
+ ].freeze
11
+
12
+ RAILS_ATTRIBUTE_OWNERS = [
13
+ defined?(ActiveRecord::Normalization) ? ActiveRecord::Normalization : ActiveModel::Attributes::Normalization,
14
+ ActiveRecord::Encryption::EncryptableRecord
15
+ ].freeze
16
+
17
+ def initialize(callback, model, defining_class)
18
+ @callback = callback
19
+ @model = model
20
+ @defining_class = defining_class
21
+ @filter = callback.filter
22
+ end
23
+
24
+ def origin
25
+ if rails_callback?
26
+ :rails
27
+ elsif external_class?
28
+ :gems
29
+ elsif !@filter.is_a?(Symbol)
30
+ :own
31
+ else
32
+ external_method?(callback_method) ? :gems : :own
33
+ end
34
+ end
35
+
36
+ def inherited?
37
+ @model != @defining_class
38
+ end
39
+
40
+ def conditional?
41
+ [@callback.instance_variable_get(:@if),
42
+ @callback.instance_variable_get(:@unless)].any? do |condition|
43
+ next false if condition.nil?
44
+ [*condition].any? { |c| c.is_a?(Symbol) || c.is_a?(Proc) }
45
+ end
46
+ end
47
+
48
+ def association_generated?
49
+ generated_by_module?("GeneratedAssociationMethods") ||
50
+ from_rails_path?(%r{/active_record/(autosave_association\.rb|associations/builder)}) ||
51
+ ValidationAnalyzer.belongs_to_validator?(@filter, @model)
52
+ end
53
+
54
+ def attribute_generated?
55
+ generated_by_module?("GeneratedAttributeMethods") ||
56
+ generated_by_rails_attributes? ||
57
+ from_rails_path?("active_record/attribute_methods/")
58
+ end
59
+
60
+ private
61
+
62
+ def rails_callback?
63
+ ValidationAnalyzer.belongs_to_validator?(@filter, @model) || standard_rails_callback?
64
+ end
65
+
66
+ def standard_rails_callback?
67
+ case @filter
68
+ when Symbol, Proc then from_rails_path?
69
+ else @defining_class == ApplicationRecord
70
+ end
71
+ end
72
+
73
+ def callback_owner
74
+ @callback_owner ||= determine_owner
75
+ end
76
+
77
+ def determine_owner
78
+ case @filter
79
+ when Symbol then callback_method&.owner
80
+ when Proc then nil
81
+ when ActiveModel::Validator, ActiveModel::EachValidator then @defining_class
82
+ else @filter.class
83
+ end
84
+ end
85
+
86
+ def callback_method
87
+ return nil unless @filter.is_a?(Symbol) || @filter.is_a?(String)
88
+
89
+ @callback_method ||= begin
90
+ @model.instance_method(@filter)
91
+ rescue
92
+ nil
93
+ end
94
+ end
95
+
96
+ def source_location
97
+ @source_location ||= case @filter
98
+ when Symbol, String then callback_method&.source_location&.first
99
+ when Proc then @filter.source_location&.first
100
+ end.to_s
101
+ end
102
+
103
+ def external_class?
104
+ @defining_class != @model && !@model.ancestors.include?(@defining_class)
105
+ end
106
+
107
+ def external_method?(method)
108
+ return false unless method
109
+
110
+ source = method.source_location&.first.to_s
111
+ !from_app_path?(source)
112
+ end
113
+
114
+ def from_app_path?(path)
115
+ path.start_with?(Rails.root.to_s) &&
116
+ !path.start_with?(Rails.root.join("vendor").to_s)
117
+ end
118
+
119
+ def generated_by_module?(suffix)
120
+ callback_method&.owner&.name&.end_with?("::" + suffix) || false
121
+ end
122
+
123
+ def generated_by_rails_attributes?
124
+ method = callback_method
125
+ return false unless method
126
+
127
+ RAILS_ATTRIBUTE_OWNERS.include?(method.owner)
128
+ end
129
+
130
+ def from_rails_path?(subpath = nil)
131
+ return false if source_location.empty?
132
+
133
+ rails_paths.any? do |rails_path|
134
+ case subpath
135
+ when String
136
+ source_location.include?("/#{subpath}")
137
+ when Regexp
138
+ source_location.match?(subpath)
139
+ else
140
+ source_location.include?(rails_path)
141
+ end
142
+ end
143
+ end
144
+
145
+ def rails_paths
146
+ @rails_paths ||= RAILS_GEMS.map { |name| Gem::Specification.find_by_name(name).full_gem_path }
147
+ end
148
+
149
+ def rails_module?(mod)
150
+ mod.name&.start_with?("ActiveRecord::", "ActiveModel::")
151
+ end
152
+
153
+ def validator?(obj)
154
+ obj.is_a?(ActiveModel::Validator)
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CallbackHell
4
+ module Analyzers
5
+ class ValidationAnalyzer
6
+ STANDARD_VALIDATIONS = %w[
7
+ presence uniqueness format length inclusion exclusion
8
+ numericality acceptance confirmation
9
+ ].freeze
10
+
11
+ STANDARD_VALIDATION_PATTERN = /^validates?_(#{STANDARD_VALIDATIONS.join("|")})(?:_of)?$/
12
+
13
+ class << self
14
+ def belongs_to_validator?(filter, model)
15
+ presence_validator?(filter) &&
16
+ association_attribute?(filter.attributes.first, model, :belongs_to)
17
+ end
18
+
19
+ def detect_type(filter, model)
20
+ return nil unless filter
21
+
22
+ if belongs_to_validator?(filter, model)
23
+ "associated"
24
+ elsif validator?(filter)
25
+ validator_type(filter)
26
+ else
27
+ normalize_validation_name(filter.to_s)
28
+ end
29
+ end
30
+
31
+ def human_method_name(filter)
32
+ case filter
33
+ when Proc then format_proc_location(filter)
34
+ when Class, ActiveModel::Validator then format_validator(filter)
35
+ else filter.to_s
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def presence_validator?(filter)
42
+ filter.is_a?(ActiveRecord::Validations::PresenceValidator)
43
+ end
44
+
45
+ def association_attribute?(attribute, model, macro)
46
+ model.reflect_on_association(attribute)&.macro == macro
47
+ end
48
+
49
+ def validator?(obj)
50
+ obj.class <= ActiveModel::EachValidator
51
+ end
52
+
53
+ def validator_type(validator)
54
+ validator.class.name.demodulize.sub("Validator", "").underscore
55
+ end
56
+
57
+ def normalize_validation_name(name)
58
+ case name
59
+ when STANDARD_VALIDATION_PATTERN, /^validate_(#{STANDARD_VALIDATIONS.join("|")})$/
60
+ $1
61
+ when /associated_records_for_/
62
+ "associated"
63
+ else
64
+ "custom"
65
+ end
66
+ end
67
+
68
+ def format_proc_location(proc)
69
+ location = proc.source_location
70
+ return "Proc (unknown location)" unless location
71
+
72
+ file = location.first.split("/").last(2).join("/")
73
+ "Proc (#{file}:#{location.last})"
74
+ end
75
+
76
+ def format_validator(validator)
77
+ "#{validator.class.name.split("::").last} (#{validator.attributes.join(", ")})"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CallbackHell
4
+ class Callback
5
+ attr_reader :model, :method_name, :conditional, :origin, :inherited, :kind,
6
+ :association_generated, :attribute_generated, :callback, :defining_class,
7
+ :fingerprint
8
+
9
+ def initialize(model:, rails_callback:, name:, defining_class:)
10
+ @model = model
11
+ @callback = rails_callback
12
+ @name = name
13
+ @defining_class = defining_class
14
+
15
+ analyzer = Analyzers::CallbackAnalyzer.new(@callback, model, defining_class)
16
+
17
+ @kind = @callback.kind
18
+ @method_name = @callback.filter
19
+ @conditional = analyzer.conditional?
20
+ @origin = analyzer.origin
21
+ @inherited = analyzer.inherited?
22
+ @association_generated = analyzer.association_generated?
23
+ @attribute_generated = analyzer.attribute_generated?
24
+ # fingerprint allows us to de-duplicate callbacks/validations;
25
+ # in most cases, it's just an object_id, but for named validations/callbacks,
26
+ # it's a combination of the name, kind and the method_name.
27
+ # The "0" and "1" prefixes define how to handle duplicates (1 means last write wins, 0 means first write wins)
28
+ @fingerprint = (@method_name.is_a?(Symbol) && @origin != :rails) ? ["1", @name, @kind, @method_name].join("-") : "0-#{@callback.object_id}"
29
+ end
30
+
31
+ def callback_group
32
+ @name.to_s
33
+ end
34
+
35
+ def validation_type
36
+ return nil unless callback_group == "validate"
37
+ Analyzers::ValidationAnalyzer.detect_type(@callback.filter, model)
38
+ end
39
+
40
+ def human_method_name
41
+ Analyzers::ValidationAnalyzer.human_method_name(@callback.filter)
42
+ end
43
+
44
+ def to_s
45
+ [
46
+ "#{model.name}: #{human_method_name}",
47
+ "kind=#{kind}_#{callback_group}",
48
+ "origin=#{origin}",
49
+ inherited ? "inherited=true" : nil,
50
+ conditional ? "conditional=true" : nil,
51
+ association_generated ? "association=true" : nil,
52
+ attribute_generated ? "attribute=true" : nil
53
+ ].compact.join(" ")
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module CallbackHell
6
+ class Collector
7
+ attr_reader :models
8
+
9
+ def initialize(models = nil, paths: nil, kind: :callbacks, mode: :default)
10
+ @paths = paths
11
+ @kind = kind
12
+ @mode = mode
13
+
14
+ eager_load!
15
+ @models = Set.new(models ? [*models] : ApplicationRecord.descendants)
16
+ end
17
+
18
+ def collect(select_models = models)
19
+ select_models.flat_map { |model| collect_for_model(model) }
20
+ end
21
+
22
+ private
23
+
24
+ def eager_load!
25
+ Rails.application.eager_load!
26
+ load_additional_paths
27
+ end
28
+
29
+ def collect_for_model(model)
30
+ model.ancestors.select { |ancestor| ancestor < ActiveRecord::Base }
31
+ # collect from parent to child to correctly handle inheritance
32
+ .reverse
33
+ .flat_map { |ancestor| collect_callbacks_for_class(model, ancestor) }
34
+ .group_by(&:fingerprint)
35
+ # merge groups
36
+ .transform_values do |callbacks|
37
+ probe = callbacks.first
38
+ if probe.fingerprint.start_with?("1")
39
+ # we must keep the last non-matching callback (i.e., if all callbacks are the same,
40
+ # we must keep the first one)
41
+ callbacks.each do |clbk|
42
+ if clbk.callback != probe.callback
43
+ probe = clbk
44
+ end
45
+ end
46
+ end
47
+ probe
48
+ end
49
+ .values
50
+ end
51
+
52
+ def collect_callbacks_for_class(model, klass)
53
+ callbacks = klass.__callbacks
54
+ callbacks = callbacks.slice(:validate) if @kind == :validations
55
+
56
+ callbacks.flat_map do |kind, chain|
57
+ chain.map { |callback| build_callback(model, callback, kind, klass) }
58
+ end.then do |collected|
59
+ next collected if @mode == :full
60
+
61
+ collected.reject do |c|
62
+ c.association_generated || c.attribute_generated || (
63
+ @kind != :validations && c.callback_group == "validate"
64
+ )
65
+ end
66
+ end
67
+ end
68
+
69
+ def build_callback(model, callback, kind, klass)
70
+ Callback.new(
71
+ model: model,
72
+ rails_callback: callback,
73
+ name: kind,
74
+ defining_class: klass
75
+ )
76
+ end
77
+
78
+ def load_additional_paths
79
+ model_paths.each do |path|
80
+ Dir[File.join(path, "**", "*.rb")].sort.each { |file| require_dependency file }
81
+ end
82
+ end
83
+
84
+ def model_paths
85
+ @model_paths ||= begin
86
+ paths = engine_paths
87
+ paths += [@paths] if @paths
88
+ paths.select(&:exist?)
89
+ end
90
+ end
91
+
92
+ def engine_paths
93
+ @engine_paths ||= Rails::Engine.subclasses.map { |engine| engine.root.join("app/models") }
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CallbackHell
4
+ class Railtie < ::Rails::Railtie
5
+ load "tasks/callback_hell.rake"
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CallbackHell
4
+ module Reports
5
+ class Base
6
+ attr_reader :callbacks, :stats
7
+
8
+ def initialize(callbacks, **opts)
9
+ @callbacks = callbacks
10
+ @stats = Stats.new(callbacks, **opts)
11
+ end
12
+
13
+ def generate
14
+ raise NotImplementedError
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CallbackHell
4
+ module Reports
5
+ module Callbacks
6
+ class Github < GithubBase
7
+ private
8
+
9
+ def report_title
10
+ "Callback Hell callbacks report"
11
+ end
12
+
13
+ def format_group_name(callback)
14
+ "#{timing_symbol(callback.kind)}/#{callback.callback_group}"
15
+ end
16
+
17
+ def timing_symbol(timing)
18
+ case timing
19
+ when :before, "before" then "⇥"
20
+ when :after, "after" then "↦"
21
+ when :around, "around" then "↔"
22
+ else
23
+ " "
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end