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.
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