packwerk 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|