packwerk 1.0.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/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
- data/.github/probots.yml +2 -0
- data/.github/pull_request_template.md +27 -0
- data/.github/workflows/ci.yml +50 -0
- data/.gitignore +12 -0
- data/.rubocop.yml +46 -0
- data/.ruby-version +1 -0
- data/CODEOWNERS +1 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/CONTRIBUTING.md +17 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +236 -0
- data/LICENSE.md +7 -0
- data/README.md +73 -0
- data/Rakefile +13 -0
- data/TROUBLESHOOT.md +67 -0
- data/USAGE.md +250 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/dev.yml +32 -0
- data/docs/cohesion.png +0 -0
- data/exe/packwerk +6 -0
- data/lib/packwerk.rb +44 -0
- data/lib/packwerk/application_validator.rb +343 -0
- data/lib/packwerk/association_inspector.rb +44 -0
- data/lib/packwerk/checking_deprecated_references.rb +40 -0
- data/lib/packwerk/cli.rb +238 -0
- data/lib/packwerk/configuration.rb +82 -0
- data/lib/packwerk/const_node_inspector.rb +44 -0
- data/lib/packwerk/constant_discovery.rb +60 -0
- data/lib/packwerk/constant_name_inspector.rb +22 -0
- data/lib/packwerk/dependency_checker.rb +28 -0
- data/lib/packwerk/deprecated_references.rb +92 -0
- data/lib/packwerk/file_processor.rb +43 -0
- data/lib/packwerk/files_for_processing.rb +67 -0
- data/lib/packwerk/formatters/progress_formatter.rb +46 -0
- data/lib/packwerk/generators/application_validation.rb +62 -0
- data/lib/packwerk/generators/configuration_file.rb +69 -0
- data/lib/packwerk/generators/inflections_file.rb +43 -0
- data/lib/packwerk/generators/root_package.rb +37 -0
- data/lib/packwerk/generators/templates/inflections.yml +6 -0
- data/lib/packwerk/generators/templates/package.yml +17 -0
- data/lib/packwerk/generators/templates/packwerk +23 -0
- data/lib/packwerk/generators/templates/packwerk.yml.erb +23 -0
- data/lib/packwerk/generators/templates/packwerk_validator_test.rb +11 -0
- data/lib/packwerk/graph.rb +74 -0
- data/lib/packwerk/inflections/custom.rb +33 -0
- data/lib/packwerk/inflections/default.rb +73 -0
- data/lib/packwerk/inflector.rb +41 -0
- data/lib/packwerk/node.rb +259 -0
- data/lib/packwerk/node_processor.rb +49 -0
- data/lib/packwerk/node_visitor.rb +22 -0
- data/lib/packwerk/offense.rb +44 -0
- data/lib/packwerk/output_styles.rb +41 -0
- data/lib/packwerk/package.rb +56 -0
- data/lib/packwerk/package_set.rb +59 -0
- data/lib/packwerk/parsed_constant_definitions.rb +62 -0
- data/lib/packwerk/parsers.rb +23 -0
- data/lib/packwerk/parsers/erb.rb +66 -0
- data/lib/packwerk/parsers/factory.rb +34 -0
- data/lib/packwerk/parsers/ruby.rb +42 -0
- data/lib/packwerk/privacy_checker.rb +45 -0
- data/lib/packwerk/reference.rb +6 -0
- data/lib/packwerk/reference_extractor.rb +81 -0
- data/lib/packwerk/reference_lister.rb +23 -0
- data/lib/packwerk/run_context.rb +103 -0
- data/lib/packwerk/sanity_checker.rb +10 -0
- data/lib/packwerk/spring_command.rb +28 -0
- data/lib/packwerk/updating_deprecated_references.rb +51 -0
- data/lib/packwerk/version.rb +6 -0
- data/lib/packwerk/violation_type.rb +13 -0
- data/library.yml +6 -0
- data/packwerk.gemspec +58 -0
- data/service.yml +6 -0
- data/shipit.rubygems.yml +1 -0
- data/sorbet/config +2 -0
- data/sorbet/rbi/gems/actioncable@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +840 -0
- data/sorbet/rbi/gems/actionmailbox@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +571 -0
- data/sorbet/rbi/gems/actionmailer@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +568 -0
- data/sorbet/rbi/gems/actionpack@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +5216 -0
- data/sorbet/rbi/gems/actiontext@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +663 -0
- data/sorbet/rbi/gems/actionview@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +2504 -0
- data/sorbet/rbi/gems/activejob@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +635 -0
- data/sorbet/rbi/gems/activemodel@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +1201 -0
- data/sorbet/rbi/gems/activerecord@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +8011 -0
- data/sorbet/rbi/gems/activestorage@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +904 -0
- data/sorbet/rbi/gems/activesupport@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +3888 -0
- data/sorbet/rbi/gems/ast@2.4.1.rbi +54 -0
- data/sorbet/rbi/gems/better_html@1.0.15.rbi +317 -0
- data/sorbet/rbi/gems/builder@3.2.4.rbi +8 -0
- data/sorbet/rbi/gems/byebug@11.1.3.rbi +8 -0
- data/sorbet/rbi/gems/coderay@1.1.3.rbi +8 -0
- data/sorbet/rbi/gems/colorize@0.8.1.rbi +40 -0
- data/sorbet/rbi/gems/commander@4.5.2.rbi +8 -0
- data/sorbet/rbi/gems/concurrent-ruby@1.1.6.rbi +1966 -0
- data/sorbet/rbi/gems/constant_resolver@0.1.5.rbi +26 -0
- data/sorbet/rbi/gems/crass@1.0.6.rbi +138 -0
- data/sorbet/rbi/gems/erubi@1.9.0.rbi +39 -0
- data/sorbet/rbi/gems/globalid@0.4.2.rbi +178 -0
- data/sorbet/rbi/gems/highline@2.0.3.rbi +8 -0
- data/sorbet/rbi/gems/html_tokenizer@0.0.7.rbi +46 -0
- data/sorbet/rbi/gems/i18n@1.8.2.rbi +633 -0
- data/sorbet/rbi/gems/jaro_winkler@1.5.4.rbi +8 -0
- data/sorbet/rbi/gems/loofah@2.5.0.rbi +272 -0
- data/sorbet/rbi/gems/m@1.5.1.rbi +108 -0
- data/sorbet/rbi/gems/mail@2.7.1.rbi +2490 -0
- data/sorbet/rbi/gems/marcel@0.3.3.rbi +30 -0
- data/sorbet/rbi/gems/method_source@1.0.0.rbi +76 -0
- data/sorbet/rbi/gems/mimemagic@0.3.5.rbi +47 -0
- data/sorbet/rbi/gems/mini_mime@1.0.2.rbi +71 -0
- data/sorbet/rbi/gems/mini_portile2@2.4.0.rbi +8 -0
- data/sorbet/rbi/gems/minitest@5.14.0.rbi +542 -0
- data/sorbet/rbi/gems/mocha@1.11.2.rbi +964 -0
- data/sorbet/rbi/gems/nio4r@2.5.2.rbi +89 -0
- data/sorbet/rbi/gems/nokogiri@1.10.9.rbi +1608 -0
- data/sorbet/rbi/gems/parallel@1.19.1.rbi +8 -0
- data/sorbet/rbi/gems/parlour@4.0.1.rbi +561 -0
- data/sorbet/rbi/gems/parser@2.7.1.4.rbi +1632 -0
- data/sorbet/rbi/gems/pry@0.13.1.rbi +8 -0
- data/sorbet/rbi/gems/rack-test@1.1.0.rbi +335 -0
- data/sorbet/rbi/gems/rack@2.2.2.rbi +1730 -0
- data/sorbet/rbi/gems/rails-dom-testing@2.0.3.rbi +123 -0
- data/sorbet/rbi/gems/rails-html-sanitizer@1.3.0.rbi +213 -0
- data/sorbet/rbi/gems/rails@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +8 -0
- data/sorbet/rbi/gems/railties@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +869 -0
- data/sorbet/rbi/gems/rainbow@3.0.0.rbi +155 -0
- data/sorbet/rbi/gems/rake@13.0.1.rbi +841 -0
- data/sorbet/rbi/gems/rexml@3.2.4.rbi +8 -0
- data/sorbet/rbi/gems/rubocop-performance@1.5.2.rbi +8 -0
- data/sorbet/rbi/gems/rubocop-shopify@1.0.2.rbi +8 -0
- data/sorbet/rbi/gems/rubocop-sorbet@0.3.7.rbi +8 -0
- data/sorbet/rbi/gems/rubocop@0.82.0.rbi +8 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.10.1.rbi +8 -0
- data/sorbet/rbi/gems/smart_properties@1.15.0.rbi +168 -0
- data/sorbet/rbi/gems/spoom@1.0.4.rbi +418 -0
- data/sorbet/rbi/gems/spring@2.1.0.rbi +160 -0
- data/sorbet/rbi/gems/sprockets-rails@3.2.1.rbi +431 -0
- data/sorbet/rbi/gems/sprockets@4.0.0.rbi +1132 -0
- data/sorbet/rbi/gems/tapioca@0.4.5.rbi +518 -0
- data/sorbet/rbi/gems/thor@1.0.1.rbi +892 -0
- data/sorbet/rbi/gems/tzinfo@2.0.2.rbi +547 -0
- data/sorbet/rbi/gems/unicode-display_width@1.7.0.rbi +8 -0
- data/sorbet/rbi/gems/websocket-driver@0.7.1.rbi +438 -0
- data/sorbet/rbi/gems/websocket-extensions@0.1.4.rbi +71 -0
- data/sorbet/rbi/gems/zeitwerk@2.3.0.rbi +8 -0
- data/sorbet/tapioca/require.rb +25 -0
- data/static/packwerk-check-demo.png +0 -0
- data/static/packwerk_check.gif +0 -0
- data/static/packwerk_check_violation.gif +0 -0
- data/static/packwerk_update.gif +0 -0
- data/static/packwerk_validate.gif +0 -0
- metadata +341 -0
data/lib/packwerk/cli.rb
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
require "benchmark"
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
require "packwerk/application_validator"
|
7
|
+
require "packwerk/configuration"
|
8
|
+
require "packwerk/files_for_processing"
|
9
|
+
require "packwerk/formatters/progress_formatter"
|
10
|
+
require "packwerk/inflector"
|
11
|
+
require "packwerk/output_styles"
|
12
|
+
require "packwerk/run_context"
|
13
|
+
require "packwerk/updating_deprecated_references"
|
14
|
+
|
15
|
+
module Packwerk
|
16
|
+
class Cli
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
def initialize(run_context: nil, configuration: nil, out: $stdout, err_out: $stderr, style: OutputStyles::Plain)
|
20
|
+
@out = out
|
21
|
+
@err_out = err_out
|
22
|
+
@style = style
|
23
|
+
@configuration = configuration || Configuration.from_path
|
24
|
+
@run_context = run_context || Packwerk::RunContext.from_configuration(@configuration)
|
25
|
+
@progress_formatter = Formatters::ProgressFormatter.new(@out, style: style)
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { params(args: T::Array[String]).returns(T.noreturn) }
|
29
|
+
def run(args)
|
30
|
+
success = execute_command(args)
|
31
|
+
exit(success)
|
32
|
+
end
|
33
|
+
|
34
|
+
sig { params(args: T::Array[String]).returns(T::Boolean) }
|
35
|
+
def execute_command(args)
|
36
|
+
subcommand = args.shift
|
37
|
+
case subcommand
|
38
|
+
when "init"
|
39
|
+
init
|
40
|
+
when "generate_configs"
|
41
|
+
generate_configs
|
42
|
+
when "check"
|
43
|
+
check(args)
|
44
|
+
when "update"
|
45
|
+
update(args)
|
46
|
+
when "validate"
|
47
|
+
validate(args)
|
48
|
+
when nil, "help"
|
49
|
+
@err_out.puts(<<~USAGE)
|
50
|
+
Usage: #{$PROGRAM_NAME} <subcommand>
|
51
|
+
|
52
|
+
Subcommands:
|
53
|
+
init - set up packwerk
|
54
|
+
check - run all checks
|
55
|
+
update - update deprecated references
|
56
|
+
validate - verify integrity of packwerk and package configuration
|
57
|
+
help - display help information about packwerk
|
58
|
+
USAGE
|
59
|
+
true
|
60
|
+
else
|
61
|
+
@err_out.puts("'#{subcommand}' is not a packwerk command. See `packwerk help`.")
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def init
|
69
|
+
@out.puts("📦 Initializing Packwerk...")
|
70
|
+
|
71
|
+
application_validation = Packwerk::Generators::ApplicationValidation.generate(
|
72
|
+
for_rails_app: rails_app?,
|
73
|
+
root: @configuration.root_path,
|
74
|
+
out: @out
|
75
|
+
)
|
76
|
+
|
77
|
+
if application_validation
|
78
|
+
if rails_app?
|
79
|
+
# To run in the same space as the Rails process,
|
80
|
+
# in order to fetch load paths for the configuration generator
|
81
|
+
exec("bin/packwerk", "generate_configs")
|
82
|
+
else
|
83
|
+
generate_configurations = generate_configs
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
application_validation && generate_configurations
|
88
|
+
end
|
89
|
+
|
90
|
+
def generate_configs
|
91
|
+
configuration_file = Packwerk::Generators::ConfigurationFile.generate(
|
92
|
+
load_paths: @configuration.load_paths,
|
93
|
+
root: @configuration.root_path,
|
94
|
+
out: @out
|
95
|
+
)
|
96
|
+
inflections_file = Packwerk::Generators::InflectionsFile.generate(root: @configuration.root_path, out: @out)
|
97
|
+
root_package = Packwerk::Generators::RootPackage.generate(root: @configuration.root_path, out: @out)
|
98
|
+
|
99
|
+
success = configuration_file && inflections_file && root_package
|
100
|
+
|
101
|
+
result = if success
|
102
|
+
<<~EOS
|
103
|
+
|
104
|
+
🎉 Packwerk is ready to be used. You can start defining packages and run `packwerk check`.
|
105
|
+
For more information on how to use Packwerk, see: https://github.com/Shopify/packwerk/blob/main/USAGE.md
|
106
|
+
EOS
|
107
|
+
else
|
108
|
+
<<~EOS
|
109
|
+
|
110
|
+
⚠️ Packwerk is not ready to be used.
|
111
|
+
Please check output and refer to https://github.com/Shopify/packwerk/blob/main/USAGE.md for more information.
|
112
|
+
EOS
|
113
|
+
end
|
114
|
+
|
115
|
+
@out.puts(result)
|
116
|
+
success
|
117
|
+
end
|
118
|
+
|
119
|
+
def update(paths)
|
120
|
+
updating_deprecated_references = ::Packwerk::UpdatingDeprecatedReferences.new(@configuration.root_path)
|
121
|
+
@run_context = Packwerk::RunContext.from_configuration(
|
122
|
+
@configuration,
|
123
|
+
reference_lister: updating_deprecated_references
|
124
|
+
)
|
125
|
+
|
126
|
+
files = fetch_files_to_process(paths)
|
127
|
+
|
128
|
+
@progress_formatter.started(files)
|
129
|
+
|
130
|
+
all_offenses = T.let([], T.untyped)
|
131
|
+
execution_time = Benchmark.realtime do
|
132
|
+
all_offenses = files.flat_map do |path|
|
133
|
+
@run_context.file_processor.call(path).tap { |offenses| mark_progress(offenses) }
|
134
|
+
end
|
135
|
+
|
136
|
+
updating_deprecated_references.dump_deprecated_references_files
|
137
|
+
end
|
138
|
+
|
139
|
+
@out.puts # put a new line after the progress dots
|
140
|
+
show_offenses(all_offenses)
|
141
|
+
@progress_formatter.finished(execution_time)
|
142
|
+
@out.puts("✅ `deprecated_references.yml` has been updated.")
|
143
|
+
|
144
|
+
all_offenses.empty?
|
145
|
+
end
|
146
|
+
|
147
|
+
def check(paths)
|
148
|
+
files = fetch_files_to_process(paths)
|
149
|
+
|
150
|
+
@progress_formatter.started(files)
|
151
|
+
|
152
|
+
all_offenses = T.let([], T.untyped)
|
153
|
+
execution_time = Benchmark.realtime do
|
154
|
+
files.each do |path|
|
155
|
+
@run_context.file_processor.call(path).tap do |offenses|
|
156
|
+
mark_progress(offenses)
|
157
|
+
all_offenses.concat(offenses)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
rescue Interrupt
|
161
|
+
@out.puts
|
162
|
+
@out.puts("Manually interrupted. Violations caught so far are listed below:")
|
163
|
+
end
|
164
|
+
|
165
|
+
@out.puts # put a new line after the progress dots
|
166
|
+
show_offenses(all_offenses)
|
167
|
+
@progress_formatter.finished(execution_time)
|
168
|
+
|
169
|
+
all_offenses.empty?
|
170
|
+
end
|
171
|
+
|
172
|
+
def fetch_files_to_process(paths)
|
173
|
+
files = FilesForProcessing.fetch(paths: paths, configuration: @configuration)
|
174
|
+
abort("No files found or given. "\
|
175
|
+
"Specify files or check the include and exclude glob in the config file.") if files.empty?
|
176
|
+
files
|
177
|
+
end
|
178
|
+
|
179
|
+
def mark_progress(offenses)
|
180
|
+
if offenses.empty?
|
181
|
+
@progress_formatter.mark_as_inspected
|
182
|
+
else
|
183
|
+
@progress_formatter.mark_as_failed
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def validate(_paths)
|
188
|
+
warn("`packwerk validate` should be run within the application. "\
|
189
|
+
"Generate the bin script using `packwerk init` and"\
|
190
|
+
" use `bin/packwerk validate` instead.") unless defined?(::Rails)
|
191
|
+
|
192
|
+
@progress_formatter.started_validation do
|
193
|
+
checker = Packwerk::ApplicationValidator.new(
|
194
|
+
config_file_path: @configuration.config_path,
|
195
|
+
application_load_paths: @configuration.all_application_autoload_paths,
|
196
|
+
configuration: @configuration
|
197
|
+
)
|
198
|
+
result = checker.check_all
|
199
|
+
|
200
|
+
list_validation_errors(result)
|
201
|
+
|
202
|
+
return result.ok?
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def show_offenses(offenses)
|
207
|
+
if offenses.empty?
|
208
|
+
@out.puts("No offenses detected 🎉")
|
209
|
+
else
|
210
|
+
offenses.each do |offense|
|
211
|
+
@out.puts(offense.to_s(@style))
|
212
|
+
end
|
213
|
+
|
214
|
+
offenses_string = Inflector.default.pluralize("offense", offenses.length)
|
215
|
+
@out.puts("#{offenses.length} #{offenses_string} detected")
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def list_validation_errors(result)
|
220
|
+
@out.puts
|
221
|
+
if result.ok?
|
222
|
+
@out.puts("Validation successful 🎉")
|
223
|
+
else
|
224
|
+
@out.puts("Validation failed ❗")
|
225
|
+
@out.puts(result.error_value)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
sig { returns(T::Boolean) }
|
230
|
+
def rails_app?
|
231
|
+
if File.exist?("config/application.rb") && File.exist?("bin/rails")
|
232
|
+
File.foreach("Gemfile").any? { |line| line.match?(/['"]rails['"]/) }
|
233
|
+
else
|
234
|
+
false
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "pathname"
|
5
|
+
require "yaml"
|
6
|
+
|
7
|
+
module Packwerk
|
8
|
+
class Configuration
|
9
|
+
class << self
|
10
|
+
def from_path(path = Dir.pwd)
|
11
|
+
raise ArgumentError, "#{File.expand_path(path)} does not exist" unless File.exist?(path)
|
12
|
+
|
13
|
+
default_packwerk_path = File.join(path, DEFAULT_CONFIG_PATH)
|
14
|
+
|
15
|
+
if File.file?(default_packwerk_path)
|
16
|
+
from_packwerk_config(default_packwerk_path)
|
17
|
+
else
|
18
|
+
new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def from_packwerk_config(path)
|
25
|
+
new(YAML.load_file(path), config_path: path)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
DEFAULT_CONFIG_PATH = "packwerk.yml"
|
30
|
+
DEFAULT_INCLUDE_GLOBS = ["**/*.{rb,rake,erb}"]
|
31
|
+
DEFAULT_EXCLUDE_GLOBS = ["{bin,node_modules,script,tmp}/**/*"]
|
32
|
+
|
33
|
+
attr_reader(
|
34
|
+
:include, :exclude, :root_path, :package_paths, :custom_associations, :load_paths, :inflections_file,
|
35
|
+
:config_path,
|
36
|
+
)
|
37
|
+
|
38
|
+
def initialize(configs = {}, config_path: nil)
|
39
|
+
@include = configs["include"] || DEFAULT_INCLUDE_GLOBS
|
40
|
+
@exclude = configs["exclude"] || DEFAULT_EXCLUDE_GLOBS
|
41
|
+
root = config_path ? File.dirname(config_path) : "."
|
42
|
+
@root_path = File.expand_path(root)
|
43
|
+
@package_paths = configs["package_paths"] || "**/"
|
44
|
+
@custom_associations = configs["custom_associations"] || []
|
45
|
+
@load_paths = configs["load_paths"] || all_application_autoload_paths
|
46
|
+
@inflections_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
|
47
|
+
|
48
|
+
@config_path = config_path
|
49
|
+
end
|
50
|
+
|
51
|
+
def all_application_autoload_paths
|
52
|
+
return [] unless defined?(::Rails)
|
53
|
+
|
54
|
+
all_paths = Rails.application.railties
|
55
|
+
.select { |railtie| railtie.is_a?(Rails::Engine) }
|
56
|
+
.push(Rails.application)
|
57
|
+
.flat_map do |engine|
|
58
|
+
(engine.config.autoload_paths + engine.config.eager_load_paths + engine.config.autoload_once_paths).uniq
|
59
|
+
end
|
60
|
+
|
61
|
+
all_paths = all_paths.map do |path_string|
|
62
|
+
# ignore paths outside of the Rails root
|
63
|
+
path = Pathname.new(path_string)
|
64
|
+
if path.exist? && path.realpath.fnmatch(Rails.root.join("**").to_s)
|
65
|
+
path.relative_path_from(Rails.root).to_s
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
all_paths.compact.tap do |paths|
|
70
|
+
if paths.empty?
|
71
|
+
raise <<~EOS
|
72
|
+
No autoload paths have been set up in your Rails app. This is likely a bug, and
|
73
|
+
packwerk is unlikely to work correctly without any autoload paths.
|
74
|
+
|
75
|
+
You can follow the Rails guides on setting up load paths, or manually configure
|
76
|
+
them in `packwerk.yml` with `load_paths`.
|
77
|
+
EOS
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "packwerk/constant_name_inspector"
|
5
|
+
|
6
|
+
module Packwerk
|
7
|
+
# Extracts a constant name from an AST node of type :const
|
8
|
+
class ConstNodeInspector
|
9
|
+
include ConstantNameInspector
|
10
|
+
|
11
|
+
def constant_name_from_node(node, ancestors:)
|
12
|
+
return nil unless Node.type(node) == Node::CONSTANT
|
13
|
+
|
14
|
+
# Only process the root `const` node for namespaced constant references. For example, in the
|
15
|
+
# reference `Spam::Eggs::Thing`, we only process the const node associated with `Spam`.
|
16
|
+
parent = ancestors.first
|
17
|
+
return nil if parent && Node.type(parent) == Node::CONSTANT
|
18
|
+
|
19
|
+
if constant_in_module_or_class_definition?(node, parent: parent)
|
20
|
+
# We're defining a class with this name, in which case the constant is implicitly fully qualified by its
|
21
|
+
# enclosing namespace
|
22
|
+
name = Node.parent_module_name(ancestors: ancestors)
|
23
|
+
name ||= Node.enclosing_namespace_path(node, ancestors: ancestors).push(Node.constant_name(node)).join("::")
|
24
|
+
|
25
|
+
"::" + name
|
26
|
+
else
|
27
|
+
begin
|
28
|
+
Node.constant_name(node)
|
29
|
+
rescue Node::TypeError
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def constant_in_module_or_class_definition?(node, parent:)
|
38
|
+
if parent
|
39
|
+
parent_name = Node.module_name_from_definition(parent)
|
40
|
+
parent_name && parent_name == Node.constant_name(node)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Packwerk
|
5
|
+
# Get information about (partially qualified) constants without loading the application code.
|
6
|
+
# Information gathered: Fully qualified name, path to file containing the definition, package,
|
7
|
+
# and visibility (public/private to the package).
|
8
|
+
#
|
9
|
+
# The implementation makes a few assumptions about the code base:
|
10
|
+
# - `Something::SomeOtherThing` is defined in a path of either `something/some_other_thing.rb` or `something.rb`,
|
11
|
+
# relative to the load path. Rails' `zeitwerk` autoloader makes the same assumption.
|
12
|
+
# - It is OK to not always infer the exact file defining the constant. For example, when a constant is inherited, we
|
13
|
+
# have no way of inferring the file it is defined in. You could argue though that inheritance means that another
|
14
|
+
# constant with the same name exists in the inheriting class, and this view is sufficient for all our use cases.
|
15
|
+
class ConstantDiscovery
|
16
|
+
ConstantContext = Struct.new(:name, :location, :package, :public?)
|
17
|
+
|
18
|
+
# @param constant_resolver [ConstantResolver]
|
19
|
+
# @param packages [Packwerk::PackageSet]
|
20
|
+
def initialize(constant_resolver:, packages:)
|
21
|
+
@packages = packages
|
22
|
+
@resolver = constant_resolver
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get the package that owns a given file path.
|
26
|
+
#
|
27
|
+
# @param path [String] the file path
|
28
|
+
#
|
29
|
+
# @return [Packwerk::Package] the package that contains the given file,
|
30
|
+
# or nil if the path is not owned by any component
|
31
|
+
def package_from_path(path)
|
32
|
+
@packages.package_from_path(path)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Analyze a constant via its name.
|
36
|
+
# If the name is partially qualified, we need the current namespace path to correctly infer its full name
|
37
|
+
#
|
38
|
+
# @param const_name [String] The constant's name, fully or partially qualified.
|
39
|
+
# @param current_namespace_path [Array<String>] (optional) The namespace of the context in which the constant is
|
40
|
+
# used, e.g. ["Apps", "Models"] for `Apps::Models`. Defaults to [] which means top level.
|
41
|
+
# @return [Packwerk::ConstantDiscovery::ConstantContext]
|
42
|
+
def context_for(const_name, current_namespace_path: [])
|
43
|
+
begin
|
44
|
+
constant = @resolver.resolve(const_name, current_namespace_path: current_namespace_path)
|
45
|
+
rescue ConstantResolver::Error => e
|
46
|
+
raise(ConstantResolver::Error, e.message + "\n Make sure autoload paths are added to the config file.")
|
47
|
+
end
|
48
|
+
|
49
|
+
return unless constant
|
50
|
+
|
51
|
+
package = @packages.package_from_path(constant.location)
|
52
|
+
ConstantContext.new(
|
53
|
+
constant.name,
|
54
|
+
constant.location,
|
55
|
+
package,
|
56
|
+
package&.public_path?(constant.location),
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "ast"
|
5
|
+
require "sorbet-runtime"
|
6
|
+
|
7
|
+
module Packwerk
|
8
|
+
# An interface describing some object that can extract a constant name from an AST node
|
9
|
+
module ConstantNameInspector
|
10
|
+
extend T::Sig
|
11
|
+
extend T::Helpers
|
12
|
+
|
13
|
+
interface!
|
14
|
+
|
15
|
+
sig do
|
16
|
+
params(node: ::AST::Node, ancestors: T::Array[::AST::Node])
|
17
|
+
.returns(T.nilable(String))
|
18
|
+
.abstract
|
19
|
+
end
|
20
|
+
def constant_name_from_node(node, ancestors:); end
|
21
|
+
end
|
22
|
+
end
|