lagoon 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/Appraisals +39 -0
- data/CHANGELOG.md +14 -2
- data/README.md +111 -146
- data/Rakefile +17 -3
- data/exe/lagoon +3 -3
- data/gemfiles/adapters.gemfile +15 -0
- data/gemfiles/rails_6.1.gemfile +23 -0
- data/gemfiles/rails_7.0.gemfile +23 -0
- data/gemfiles/rails_7.1.gemfile +23 -0
- data/gemfiles/rails_7.2.gemfile +23 -0
- data/gemfiles/rails_8.0.gemfile +23 -0
- data/gemfiles/rails_8.1.gemfile +23 -0
- data/lib/lagoon/analyzer/action_controller_analyzer.rb +76 -0
- data/lib/lagoon/analyzer/active_record_analyzer.rb +202 -0
- data/lib/lagoon/analyzer/ast/controller_scope_collector.rb +104 -0
- data/lib/lagoon/analyzer/ast/method_reference_visitor.rb +170 -0
- data/lib/lagoon/analyzer/ast_model_reference_analyzer.rb +44 -0
- data/lib/lagoon/analyzer/database_schema_analyzer.rb +83 -0
- data/lib/lagoon/cli.rb +109 -80
- data/lib/lagoon/configuration.rb +42 -6
- data/lib/lagoon/diagram/base.rb +48 -8
- data/lib/lagoon/diagram/controller_diagram.rb +10 -16
- data/lib/lagoon/diagram/controller_model_diagram.rb +28 -0
- data/lib/lagoon/diagram/er_diagram.rb +10 -16
- data/lib/lagoon/diagram/model_diagram.rb +13 -16
- data/lib/lagoon/errors.rb +9 -0
- data/lib/lagoon/options.rb +182 -0
- data/lib/lagoon/parser/application_class_filter.rb +47 -0
- data/lib/lagoon/parser/controller_model_parser.rb +196 -0
- data/lib/lagoon/parser/controller_parser.rb +37 -54
- data/lib/lagoon/parser/model_parser.rb +57 -112
- data/lib/lagoon/parser/schema_parser.rb +120 -67
- data/lib/lagoon/railtie.rb +1 -1
- data/lib/lagoon/renderer/base_renderer.rb +49 -19
- data/lib/lagoon/renderer/class_diagram_renderer.rb +37 -31
- data/lib/lagoon/renderer/controller_model_er_renderer.rb +47 -0
- data/lib/lagoon/renderer/er_diagram_renderer.rb +43 -42
- data/lib/lagoon/result.rb +22 -0
- data/lib/lagoon/version.rb +1 -1
- data/lib/lagoon.rb +70 -21
- data/lib/tasks/lagoon.rake +15 -7
- data/sig/lagoon.rbs +107 -0
- metadata +57 -7
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lagoon
|
|
4
|
+
class Options
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
COMMON_KEYS = %i[output verbose strict eager_load].freeze
|
|
8
|
+
KEYS = {
|
|
9
|
+
model: COMMON_KEYS + %i[
|
|
10
|
+
direction brief show_attributes show_methods include_inheritance exclude specify
|
|
11
|
+
all_models show_belongs_to hide_through all_columns hide_magic hide_types
|
|
12
|
+
include_framework_bases duplicate_sti_attributes
|
|
13
|
+
],
|
|
14
|
+
controller: COMMON_KEYS + %i[
|
|
15
|
+
direction brief include_inheritance exclude specify all_controllers hide_public
|
|
16
|
+
hide_protected hide_private include_framework_bases
|
|
17
|
+
],
|
|
18
|
+
er: COMMON_KEYS + %i[exclude specify connections internal_tables],
|
|
19
|
+
controller_model: COMMON_KEYS + %i[
|
|
20
|
+
direction exclude specify all_controllers show_actions helper_models
|
|
21
|
+
]
|
|
22
|
+
}.transform_values(&:freeze).freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :kind
|
|
25
|
+
|
|
26
|
+
def self.for(kind, raw_options = nil, config: Lagoon.configuration, **keyword_options)
|
|
27
|
+
options = (raw_options || {}).to_h.merge(keyword_options)
|
|
28
|
+
new(kind, options, config: config)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.allowed_keys(kind)
|
|
32
|
+
KEYS.fetch(kind)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(kind, raw_options = {}, config: Lagoon.configuration)
|
|
36
|
+
@kind = kind.to_sym
|
|
37
|
+
@config = config
|
|
38
|
+
@raw = symbolize_keys(raw_options)
|
|
39
|
+
validate_keys!
|
|
40
|
+
@values = normalize.freeze
|
|
41
|
+
freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def [](key)
|
|
45
|
+
@values[key.to_sym]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def fetch(key, *defaults, &)
|
|
49
|
+
@values.fetch(key.to_sym, *defaults, &)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def key?(key)
|
|
53
|
+
@values.key?(key.to_sym)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def each(&)
|
|
57
|
+
@values.each(&)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_h
|
|
61
|
+
@values.dup
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def symbolize_keys(raw_options)
|
|
67
|
+
raw_options.to_h.transform_keys(&:to_sym)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_keys!
|
|
71
|
+
unknown = @raw.keys - self.class.allowed_keys(@kind)
|
|
72
|
+
return if unknown.empty?
|
|
73
|
+
|
|
74
|
+
raise ConfigurationError, "Unknown #{@kind} option(s): #{unknown.sort.join(', ')}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def normalize
|
|
78
|
+
values = common_values
|
|
79
|
+
case @kind
|
|
80
|
+
when :model then values.merge(model_values)
|
|
81
|
+
when :controller then values.merge(controller_values)
|
|
82
|
+
when :er then values.merge(er_values)
|
|
83
|
+
when :controller_model then values.merge(controller_model_values)
|
|
84
|
+
else raise ConfigurationError, "Unknown diagram kind: #{@kind}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def common_values
|
|
89
|
+
{
|
|
90
|
+
output: optional_path(@raw[:output]),
|
|
91
|
+
verbose: boolean(:verbose, false),
|
|
92
|
+
strict: boolean(:strict, @config.strict),
|
|
93
|
+
eager_load: boolean(:eager_load, true)
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def model_values
|
|
98
|
+
brief = boolean(:brief, false)
|
|
99
|
+
{
|
|
100
|
+
direction: direction,
|
|
101
|
+
brief: brief,
|
|
102
|
+
show_attributes: brief ? false : boolean(:show_attributes, @config.show_attributes),
|
|
103
|
+
show_methods: brief ? false : boolean(:show_methods, @config.show_methods),
|
|
104
|
+
include_inheritance: boolean(:include_inheritance, @config.include_inheritance),
|
|
105
|
+
exclude: names(@config.exclude_models, @raw[:exclude]),
|
|
106
|
+
specify: names(@raw[:specify]),
|
|
107
|
+
all_models: boolean(:all_models, false),
|
|
108
|
+
show_belongs_to: boolean(:show_belongs_to, false),
|
|
109
|
+
hide_through: boolean(:hide_through, false),
|
|
110
|
+
all_columns: boolean(:all_columns, false),
|
|
111
|
+
hide_magic: boolean(:hide_magic, false),
|
|
112
|
+
hide_types: boolean(:hide_types, false),
|
|
113
|
+
include_framework_bases: boolean(:include_framework_bases, @config.include_framework_bases),
|
|
114
|
+
duplicate_sti_attributes: boolean(:duplicate_sti_attributes, false)
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def controller_values
|
|
119
|
+
brief = boolean(:brief, false)
|
|
120
|
+
{
|
|
121
|
+
direction: direction,
|
|
122
|
+
brief: brief,
|
|
123
|
+
include_inheritance: boolean(:include_inheritance, @config.include_inheritance),
|
|
124
|
+
exclude: names(@config.exclude_controllers, @raw[:exclude]),
|
|
125
|
+
specify: names(@raw[:specify]),
|
|
126
|
+
all_controllers: boolean(:all_controllers, false),
|
|
127
|
+
hide_public: brief || boolean(:hide_public, false),
|
|
128
|
+
hide_protected: brief || boolean(:hide_protected, false),
|
|
129
|
+
hide_private: brief || boolean(:hide_private, false),
|
|
130
|
+
include_framework_bases: boolean(:include_framework_bases, @config.include_framework_bases)
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def er_values
|
|
135
|
+
{
|
|
136
|
+
exclude: names(@config.exclude_tables, @raw[:exclude]),
|
|
137
|
+
specify: names(@raw[:specify]),
|
|
138
|
+
connections: @raw[:connections],
|
|
139
|
+
internal_tables: names(@raw.fetch(:internal_tables, @config.internal_tables))
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def controller_model_values
|
|
144
|
+
{
|
|
145
|
+
direction: direction,
|
|
146
|
+
exclude: names(@config.exclude_controllers, @raw[:exclude]),
|
|
147
|
+
specify: names(@raw[:specify]),
|
|
148
|
+
all_controllers: boolean(:all_controllers, false),
|
|
149
|
+
show_actions: boolean(:show_actions, true),
|
|
150
|
+
helper_models: normalize_helper_models(@raw.fetch(:helper_models, @config.helper_models))
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def direction
|
|
155
|
+
value = @raw.fetch(:direction, @config.diagram_direction)
|
|
156
|
+
Configuration.validate_direction!(value)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def boolean(key, default)
|
|
160
|
+
@raw.key?(key) ? !!@raw[key] : default
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def names(*groups)
|
|
164
|
+
groups.compact.flatten.map(&:to_s).reject(&:empty?).uniq.sort.freeze
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def normalize_helper_models(mapping)
|
|
168
|
+
mapping.to_h.each_with_object({}) do |(helper, model), result|
|
|
169
|
+
result[helper.to_s] = model.to_s
|
|
170
|
+
end.freeze
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def optional_path(path)
|
|
174
|
+
return nil if path.nil?
|
|
175
|
+
|
|
176
|
+
value = path.to_s
|
|
177
|
+
raise ConfigurationError, 'Output path cannot be empty' if value.empty?
|
|
178
|
+
|
|
179
|
+
value.freeze
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'active_support'
|
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
|
6
|
+
|
|
7
|
+
module Lagoon
|
|
8
|
+
module Parser
|
|
9
|
+
class ApplicationClassFilter
|
|
10
|
+
def initialize(directory:, include_all: false)
|
|
11
|
+
@directory = directory
|
|
12
|
+
@include_all = include_all
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def include?(klass)
|
|
16
|
+
return false unless klass.name
|
|
17
|
+
return true if @include_all
|
|
18
|
+
return true unless defined?(Rails) && Rails.respond_to?(:root)
|
|
19
|
+
|
|
20
|
+
application_directory = File.expand_path(File.join(Rails.root.to_s, 'app', @directory))
|
|
21
|
+
source_locations(klass).any? do |location|
|
|
22
|
+
File.expand_path(location).start_with?("#{application_directory}#{File::SEPARATOR}")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def source_locations(klass)
|
|
29
|
+
locations = [constant_source_location(klass)]
|
|
30
|
+
method_names = klass.instance_methods(false) +
|
|
31
|
+
klass.private_instance_methods(false) +
|
|
32
|
+
klass.protected_instance_methods(false)
|
|
33
|
+
locations.concat(method_names.filter_map { |name| klass.instance_method(name).source_location&.first })
|
|
34
|
+
locations.compact.uniq
|
|
35
|
+
rescue NameError
|
|
36
|
+
[]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def constant_source_location(klass)
|
|
40
|
+
namespace_name = klass.name.deconstantize
|
|
41
|
+
constant_name = klass.name.demodulize.to_sym
|
|
42
|
+
namespace = namespace_name.empty? ? Object : namespace_name.safe_constantize
|
|
43
|
+
namespace&.const_source_location(constant_name)&.first
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'active_support'
|
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
|
6
|
+
|
|
7
|
+
module Lagoon
|
|
8
|
+
module Parser
|
|
9
|
+
class ControllerModelParser
|
|
10
|
+
attr_reader :options
|
|
11
|
+
|
|
12
|
+
def initialize(options = {})
|
|
13
|
+
@options = options.is_a?(Options) ? options : Options.for(:controller_model, options)
|
|
14
|
+
@filter = ApplicationClassFilter.new(
|
|
15
|
+
directory: 'controllers',
|
|
16
|
+
include_all: @options[:all_controllers]
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parse
|
|
21
|
+
relationships = []
|
|
22
|
+
warnings = []
|
|
23
|
+
model_metadata = load_model_metadata(warnings)
|
|
24
|
+
|
|
25
|
+
load_controllers.sort_by { |controller| controller.name.to_s }.each do |controller|
|
|
26
|
+
next if excluded?(controller)
|
|
27
|
+
|
|
28
|
+
analyze_controller(controller, model_metadata, relationships, warnings)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
aggregated = aggregate_relationships(relationships)
|
|
32
|
+
{
|
|
33
|
+
relationships: aggregated,
|
|
34
|
+
warnings: warnings,
|
|
35
|
+
counts: { relationships: aggregated.size, skipped: warnings.size }
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def load_controllers
|
|
42
|
+
return [] unless defined?(ActionController::Base)
|
|
43
|
+
|
|
44
|
+
Rails.application.eager_load! if options[:eager_load] && defined?(Rails)
|
|
45
|
+
ActionController::Base.descendants
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def load_model_metadata(warnings)
|
|
49
|
+
return { names: [], associations: {} } unless defined?(ActiveRecord::Base)
|
|
50
|
+
|
|
51
|
+
models = ActiveRecord::Base.descendants.select(&:name)
|
|
52
|
+
associations = models.to_h do |model|
|
|
53
|
+
[model.name, association_mapping(model, warnings)]
|
|
54
|
+
end
|
|
55
|
+
{ names: models.map(&:name), associations: associations }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def association_mapping(model, warnings)
|
|
59
|
+
model.reflect_on_all_associations.each_with_object({}) do |association, result|
|
|
60
|
+
next if association.options[:polymorphic]
|
|
61
|
+
|
|
62
|
+
result[association.name.to_s] = association.class_name
|
|
63
|
+
rescue NameError => e
|
|
64
|
+
warnings << "Could not resolve #{model.name}.#{association.name}: #{e.message}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def excluded?(controller)
|
|
69
|
+
controller_name = controller.name
|
|
70
|
+
return true unless controller_name
|
|
71
|
+
return true unless @filter.include?(controller)
|
|
72
|
+
return true if options[:exclude].include?(controller_name)
|
|
73
|
+
return !options[:specify].include?(controller_name) if options[:specify].any?
|
|
74
|
+
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def analyze_controller(controller, model_metadata, relationships, warnings)
|
|
79
|
+
actions = controller.action_methods.to_a.map(&:to_s).sort
|
|
80
|
+
return if actions.empty?
|
|
81
|
+
|
|
82
|
+
source_context = source_context_for(controller, actions)
|
|
83
|
+
if source_context[:files].empty?
|
|
84
|
+
warnings << "No source file found for #{controller.name}"
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
action_models = analyze_source_files(
|
|
89
|
+
source_context[:files],
|
|
90
|
+
actions,
|
|
91
|
+
source_context[:controller_names],
|
|
92
|
+
model_metadata
|
|
93
|
+
)
|
|
94
|
+
add_relationships(controller.name, action_models, relationships)
|
|
95
|
+
rescue Errno::ENOENT, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError, ParseError => e
|
|
96
|
+
raise if options[:strict]
|
|
97
|
+
|
|
98
|
+
warnings << "Failed to parse #{controller.name}: #{e.message}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def source_context_for(controller, actions)
|
|
102
|
+
owners = actions.filter_map do |action|
|
|
103
|
+
controller.instance_method(action).owner
|
|
104
|
+
rescue NameError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
owners << controller
|
|
108
|
+
owners.concat(callback_owners(controller))
|
|
109
|
+
owners.select!(&:name)
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
controller_names: owners.map(&:name).uniq,
|
|
113
|
+
files: owners.flat_map { |owner| source_files_for(owner) }.compact.uniq.select { |file| File.file?(file) }
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def source_files_for(controller)
|
|
118
|
+
method_names = controller.instance_methods(false) +
|
|
119
|
+
controller.private_instance_methods(false) +
|
|
120
|
+
controller.protected_instance_methods(false)
|
|
121
|
+
files = method_names.filter_map do |method_name|
|
|
122
|
+
controller.instance_method(method_name).source_location&.first
|
|
123
|
+
end
|
|
124
|
+
constant_location = constant_source_location(controller)
|
|
125
|
+
files << constant_location if constant_location
|
|
126
|
+
files << conventional_source_file(controller) if files.empty?
|
|
127
|
+
files
|
|
128
|
+
rescue NameError
|
|
129
|
+
[conventional_source_file(controller)]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def constant_source_location(controller)
|
|
133
|
+
namespace_name = controller.name.deconstantize
|
|
134
|
+
namespace = namespace_name.empty? ? Object : namespace_name.safe_constantize
|
|
135
|
+
namespace&.const_source_location(controller.name.demodulize.to_sym)&.first
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def conventional_source_file(controller)
|
|
139
|
+
return nil unless defined?(Rails) && Rails.respond_to?(:root)
|
|
140
|
+
|
|
141
|
+
Rails.root.join('app', 'controllers', "#{controller.name.underscore}.rb").to_s
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def callback_methods(controller)
|
|
145
|
+
return [] unless controller.respond_to?(:_process_action_callbacks)
|
|
146
|
+
|
|
147
|
+
controller._process_action_callbacks.filter_map do |callback|
|
|
148
|
+
callback.filter.to_s if callback.filter.is_a?(Symbol)
|
|
149
|
+
end.uniq
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def callback_owners(controller)
|
|
153
|
+
callback_methods(controller).filter_map do |method_name|
|
|
154
|
+
controller.instance_method(method_name).owner
|
|
155
|
+
rescue NameError
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def analyze_source_files(file_paths, action_names, controller_names, model_metadata)
|
|
161
|
+
require_relative '../analyzer/ast_model_reference_analyzer'
|
|
162
|
+
analyzer = Lagoon::Analyzer::AstModelReferenceAnalyzer.new
|
|
163
|
+
analyzer.analyze(
|
|
164
|
+
file_paths,
|
|
165
|
+
action_names,
|
|
166
|
+
controller_names: controller_names,
|
|
167
|
+
model_names: model_metadata[:names],
|
|
168
|
+
associations: model_metadata[:associations],
|
|
169
|
+
helper_models: options[:helper_models]
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def add_relationships(controller_name, action_models, relationships)
|
|
174
|
+
action_models.each do |action, models|
|
|
175
|
+
models.each do |model|
|
|
176
|
+
relationships << { controller: controller_name, action: action, model: model }
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def aggregate_relationships(relationships)
|
|
182
|
+
grouped = relationships.group_by { |relationship| [relationship[:controller], relationship[:model]] }
|
|
183
|
+
|
|
184
|
+
aggregated = grouped.map do |(controller, model), items|
|
|
185
|
+
{
|
|
186
|
+
controller: controller,
|
|
187
|
+
model: model,
|
|
188
|
+
actions: items.map { |item| item[:action] }.sort.uniq
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
aggregated.sort_by { |relationship| [relationship[:controller], relationship[:model]] }
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -6,8 +6,9 @@ module Lagoon
|
|
|
6
6
|
attr_reader :options, :config
|
|
7
7
|
|
|
8
8
|
def initialize(options = {})
|
|
9
|
-
@options = options
|
|
10
|
-
@
|
|
9
|
+
@options = options.is_a?(Options) ? options : Options.for(:controller, options)
|
|
10
|
+
@analyzer = Lagoon::Analyzer::ActionControllerAnalyzer.new
|
|
11
|
+
@filter = ApplicationClassFilter.new(directory: 'controllers', include_all: @options[:all_controllers])
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def parse
|
|
@@ -15,80 +16,62 @@ module Lagoon
|
|
|
15
16
|
classes = []
|
|
16
17
|
relationships = []
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
warnings = []
|
|
20
|
+
|
|
21
|
+
controllers.sort_by { |controller| controller.name.to_s }.each do |controller|
|
|
19
22
|
next if excluded?(controller)
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
controller_data = @analyzer.analyze_controller(controller, analysis_options)
|
|
25
|
+
classes << controller_data
|
|
26
|
+
|
|
27
|
+
if options[:include_inheritance]
|
|
28
|
+
relationships.concat(
|
|
29
|
+
@analyzer.extract_inheritance(
|
|
30
|
+
controller,
|
|
31
|
+
include_framework_base: options[:include_framework_bases]
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
raise if options[:strict]
|
|
37
|
+
|
|
38
|
+
warnings << "Failed to analyze controller #{controller.name || '(anonymous)'}: #{e.message}"
|
|
23
39
|
end
|
|
24
40
|
|
|
25
41
|
{
|
|
26
|
-
classes: classes,
|
|
27
|
-
relationships: relationships
|
|
42
|
+
classes: classes.sort_by { |controller| controller[:name] },
|
|
43
|
+
relationships: relationships.sort_by { |relationship| [relationship[:source], relationship[:target]] },
|
|
44
|
+
warnings: warnings,
|
|
45
|
+
counts: { classes: classes.size, relationships: relationships.size, skipped: warnings.size }
|
|
28
46
|
}
|
|
29
47
|
end
|
|
30
48
|
|
|
31
49
|
private
|
|
32
50
|
|
|
33
51
|
def load_controllers
|
|
34
|
-
|
|
35
|
-
return [] unless defined?(Rails)
|
|
52
|
+
return [] unless defined?(ActionController::Base)
|
|
36
53
|
|
|
37
|
-
Rails.application.eager_load!
|
|
54
|
+
Rails.application.eager_load! if options[:eager_load] && defined?(Rails)
|
|
38
55
|
ActionController::Base.descendants
|
|
39
56
|
end
|
|
40
57
|
|
|
41
58
|
def excluded?(controller)
|
|
42
59
|
controller_name = controller.name
|
|
43
|
-
|
|
60
|
+
return true unless controller_name
|
|
61
|
+
return true unless @filter.include?(controller)
|
|
62
|
+
return true if options[:exclude].include?(controller_name)
|
|
63
|
+
return !options[:specify].include?(controller_name) if options[:specify].any?
|
|
64
|
+
|
|
65
|
+
false
|
|
44
66
|
end
|
|
45
67
|
|
|
46
|
-
def
|
|
68
|
+
def analysis_options
|
|
47
69
|
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
methods: extract_methods(controller)
|
|
70
|
+
hide_public: options[:hide_public],
|
|
71
|
+
hide_protected: options[:hide_protected],
|
|
72
|
+
hide_private: options[:hide_private]
|
|
52
73
|
}
|
|
53
74
|
end
|
|
54
|
-
|
|
55
|
-
def extract_methods(controller)
|
|
56
|
-
methods = []
|
|
57
|
-
|
|
58
|
-
# Public methods
|
|
59
|
-
unless options[:hide_public]
|
|
60
|
-
public_methods = controller.action_methods.to_a
|
|
61
|
-
methods.concat(public_methods.map { |m| { name: m, visibility: "+" } })
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Protected methods
|
|
65
|
-
unless options[:hide_protected]
|
|
66
|
-
protected_methods = controller.protected_instance_methods(false)
|
|
67
|
-
methods.concat(protected_methods.map { |m| { name: m, visibility: "#" } })
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Private methods
|
|
71
|
-
unless options[:hide_private]
|
|
72
|
-
private_methods = controller.private_instance_methods(false)
|
|
73
|
-
methods.concat(private_methods.map { |m| { name: m, visibility: "-" } })
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
methods
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def extract_inheritance(controller)
|
|
80
|
-
return [] if controller.superclass == ActionController::Base
|
|
81
|
-
return [] unless controller.superclass.name
|
|
82
|
-
|
|
83
|
-
[{
|
|
84
|
-
source: controller.superclass.name,
|
|
85
|
-
target: controller.name,
|
|
86
|
-
type: :inheritance,
|
|
87
|
-
label: nil
|
|
88
|
-
}]
|
|
89
|
-
rescue StandardError
|
|
90
|
-
[]
|
|
91
|
-
end
|
|
92
75
|
end
|
|
93
76
|
end
|
|
94
77
|
end
|