packwerk 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
  3. data/.github/probots.yml +2 -0
  4. data/.github/pull_request_template.md +27 -0
  5. data/.github/workflows/ci.yml +50 -0
  6. data/.gitignore +12 -0
  7. data/.rubocop.yml +46 -0
  8. data/.ruby-version +1 -0
  9. data/CODEOWNERS +1 -0
  10. data/CODE_OF_CONDUCT.md +76 -0
  11. data/CONTRIBUTING.md +17 -0
  12. data/Gemfile +22 -0
  13. data/Gemfile.lock +236 -0
  14. data/LICENSE.md +7 -0
  15. data/README.md +73 -0
  16. data/Rakefile +13 -0
  17. data/TROUBLESHOOT.md +67 -0
  18. data/USAGE.md +250 -0
  19. data/bin/console +15 -0
  20. data/bin/setup +8 -0
  21. data/dev.yml +32 -0
  22. data/docs/cohesion.png +0 -0
  23. data/exe/packwerk +6 -0
  24. data/lib/packwerk.rb +44 -0
  25. data/lib/packwerk/application_validator.rb +343 -0
  26. data/lib/packwerk/association_inspector.rb +44 -0
  27. data/lib/packwerk/checking_deprecated_references.rb +40 -0
  28. data/lib/packwerk/cli.rb +238 -0
  29. data/lib/packwerk/configuration.rb +82 -0
  30. data/lib/packwerk/const_node_inspector.rb +44 -0
  31. data/lib/packwerk/constant_discovery.rb +60 -0
  32. data/lib/packwerk/constant_name_inspector.rb +22 -0
  33. data/lib/packwerk/dependency_checker.rb +28 -0
  34. data/lib/packwerk/deprecated_references.rb +92 -0
  35. data/lib/packwerk/file_processor.rb +43 -0
  36. data/lib/packwerk/files_for_processing.rb +67 -0
  37. data/lib/packwerk/formatters/progress_formatter.rb +46 -0
  38. data/lib/packwerk/generators/application_validation.rb +62 -0
  39. data/lib/packwerk/generators/configuration_file.rb +69 -0
  40. data/lib/packwerk/generators/inflections_file.rb +43 -0
  41. data/lib/packwerk/generators/root_package.rb +37 -0
  42. data/lib/packwerk/generators/templates/inflections.yml +6 -0
  43. data/lib/packwerk/generators/templates/package.yml +17 -0
  44. data/lib/packwerk/generators/templates/packwerk +23 -0
  45. data/lib/packwerk/generators/templates/packwerk.yml.erb +23 -0
  46. data/lib/packwerk/generators/templates/packwerk_validator_test.rb +11 -0
  47. data/lib/packwerk/graph.rb +74 -0
  48. data/lib/packwerk/inflections/custom.rb +33 -0
  49. data/lib/packwerk/inflections/default.rb +73 -0
  50. data/lib/packwerk/inflector.rb +41 -0
  51. data/lib/packwerk/node.rb +259 -0
  52. data/lib/packwerk/node_processor.rb +49 -0
  53. data/lib/packwerk/node_visitor.rb +22 -0
  54. data/lib/packwerk/offense.rb +44 -0
  55. data/lib/packwerk/output_styles.rb +41 -0
  56. data/lib/packwerk/package.rb +56 -0
  57. data/lib/packwerk/package_set.rb +59 -0
  58. data/lib/packwerk/parsed_constant_definitions.rb +62 -0
  59. data/lib/packwerk/parsers.rb +23 -0
  60. data/lib/packwerk/parsers/erb.rb +66 -0
  61. data/lib/packwerk/parsers/factory.rb +34 -0
  62. data/lib/packwerk/parsers/ruby.rb +42 -0
  63. data/lib/packwerk/privacy_checker.rb +45 -0
  64. data/lib/packwerk/reference.rb +6 -0
  65. data/lib/packwerk/reference_extractor.rb +81 -0
  66. data/lib/packwerk/reference_lister.rb +23 -0
  67. data/lib/packwerk/run_context.rb +103 -0
  68. data/lib/packwerk/sanity_checker.rb +10 -0
  69. data/lib/packwerk/spring_command.rb +28 -0
  70. data/lib/packwerk/updating_deprecated_references.rb +51 -0
  71. data/lib/packwerk/version.rb +6 -0
  72. data/lib/packwerk/violation_type.rb +13 -0
  73. data/library.yml +6 -0
  74. data/packwerk.gemspec +58 -0
  75. data/service.yml +6 -0
  76. data/shipit.rubygems.yml +1 -0
  77. data/sorbet/config +2 -0
  78. data/sorbet/rbi/gems/actioncable@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +840 -0
  79. data/sorbet/rbi/gems/actionmailbox@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +571 -0
  80. data/sorbet/rbi/gems/actionmailer@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +568 -0
  81. data/sorbet/rbi/gems/actionpack@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +5216 -0
  82. data/sorbet/rbi/gems/actiontext@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +663 -0
  83. data/sorbet/rbi/gems/actionview@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +2504 -0
  84. data/sorbet/rbi/gems/activejob@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +635 -0
  85. data/sorbet/rbi/gems/activemodel@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +1201 -0
  86. data/sorbet/rbi/gems/activerecord@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +8011 -0
  87. data/sorbet/rbi/gems/activestorage@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +904 -0
  88. data/sorbet/rbi/gems/activesupport@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +3888 -0
  89. data/sorbet/rbi/gems/ast@2.4.1.rbi +54 -0
  90. data/sorbet/rbi/gems/better_html@1.0.15.rbi +317 -0
  91. data/sorbet/rbi/gems/builder@3.2.4.rbi +8 -0
  92. data/sorbet/rbi/gems/byebug@11.1.3.rbi +8 -0
  93. data/sorbet/rbi/gems/coderay@1.1.3.rbi +8 -0
  94. data/sorbet/rbi/gems/colorize@0.8.1.rbi +40 -0
  95. data/sorbet/rbi/gems/commander@4.5.2.rbi +8 -0
  96. data/sorbet/rbi/gems/concurrent-ruby@1.1.6.rbi +1966 -0
  97. data/sorbet/rbi/gems/constant_resolver@0.1.5.rbi +26 -0
  98. data/sorbet/rbi/gems/crass@1.0.6.rbi +138 -0
  99. data/sorbet/rbi/gems/erubi@1.9.0.rbi +39 -0
  100. data/sorbet/rbi/gems/globalid@0.4.2.rbi +178 -0
  101. data/sorbet/rbi/gems/highline@2.0.3.rbi +8 -0
  102. data/sorbet/rbi/gems/html_tokenizer@0.0.7.rbi +46 -0
  103. data/sorbet/rbi/gems/i18n@1.8.2.rbi +633 -0
  104. data/sorbet/rbi/gems/jaro_winkler@1.5.4.rbi +8 -0
  105. data/sorbet/rbi/gems/loofah@2.5.0.rbi +272 -0
  106. data/sorbet/rbi/gems/m@1.5.1.rbi +108 -0
  107. data/sorbet/rbi/gems/mail@2.7.1.rbi +2490 -0
  108. data/sorbet/rbi/gems/marcel@0.3.3.rbi +30 -0
  109. data/sorbet/rbi/gems/method_source@1.0.0.rbi +76 -0
  110. data/sorbet/rbi/gems/mimemagic@0.3.5.rbi +47 -0
  111. data/sorbet/rbi/gems/mini_mime@1.0.2.rbi +71 -0
  112. data/sorbet/rbi/gems/mini_portile2@2.4.0.rbi +8 -0
  113. data/sorbet/rbi/gems/minitest@5.14.0.rbi +542 -0
  114. data/sorbet/rbi/gems/mocha@1.11.2.rbi +964 -0
  115. data/sorbet/rbi/gems/nio4r@2.5.2.rbi +89 -0
  116. data/sorbet/rbi/gems/nokogiri@1.10.9.rbi +1608 -0
  117. data/sorbet/rbi/gems/parallel@1.19.1.rbi +8 -0
  118. data/sorbet/rbi/gems/parlour@4.0.1.rbi +561 -0
  119. data/sorbet/rbi/gems/parser@2.7.1.4.rbi +1632 -0
  120. data/sorbet/rbi/gems/pry@0.13.1.rbi +8 -0
  121. data/sorbet/rbi/gems/rack-test@1.1.0.rbi +335 -0
  122. data/sorbet/rbi/gems/rack@2.2.2.rbi +1730 -0
  123. data/sorbet/rbi/gems/rails-dom-testing@2.0.3.rbi +123 -0
  124. data/sorbet/rbi/gems/rails-html-sanitizer@1.3.0.rbi +213 -0
  125. data/sorbet/rbi/gems/rails@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +8 -0
  126. data/sorbet/rbi/gems/railties@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +869 -0
  127. data/sorbet/rbi/gems/rainbow@3.0.0.rbi +155 -0
  128. data/sorbet/rbi/gems/rake@13.0.1.rbi +841 -0
  129. data/sorbet/rbi/gems/rexml@3.2.4.rbi +8 -0
  130. data/sorbet/rbi/gems/rubocop-performance@1.5.2.rbi +8 -0
  131. data/sorbet/rbi/gems/rubocop-shopify@1.0.2.rbi +8 -0
  132. data/sorbet/rbi/gems/rubocop-sorbet@0.3.7.rbi +8 -0
  133. data/sorbet/rbi/gems/rubocop@0.82.0.rbi +8 -0
  134. data/sorbet/rbi/gems/ruby-progressbar@1.10.1.rbi +8 -0
  135. data/sorbet/rbi/gems/smart_properties@1.15.0.rbi +168 -0
  136. data/sorbet/rbi/gems/spoom@1.0.4.rbi +418 -0
  137. data/sorbet/rbi/gems/spring@2.1.0.rbi +160 -0
  138. data/sorbet/rbi/gems/sprockets-rails@3.2.1.rbi +431 -0
  139. data/sorbet/rbi/gems/sprockets@4.0.0.rbi +1132 -0
  140. data/sorbet/rbi/gems/tapioca@0.4.5.rbi +518 -0
  141. data/sorbet/rbi/gems/thor@1.0.1.rbi +892 -0
  142. data/sorbet/rbi/gems/tzinfo@2.0.2.rbi +547 -0
  143. data/sorbet/rbi/gems/unicode-display_width@1.7.0.rbi +8 -0
  144. data/sorbet/rbi/gems/websocket-driver@0.7.1.rbi +438 -0
  145. data/sorbet/rbi/gems/websocket-extensions@0.1.4.rbi +71 -0
  146. data/sorbet/rbi/gems/zeitwerk@2.3.0.rbi +8 -0
  147. data/sorbet/tapioca/require.rb +25 -0
  148. data/static/packwerk-check-demo.png +0 -0
  149. data/static/packwerk_check.gif +0 -0
  150. data/static/packwerk_check_violation.gif +0 -0
  151. data/static/packwerk_update.gif +0 -0
  152. data/static/packwerk_validate.gif +0 -0
  153. metadata +341 -0
@@ -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