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,49 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "packwerk/node"
5
+ require "packwerk/offense"
6
+
7
+ module Packwerk
8
+ class NodeProcessor
9
+ def initialize(reference_extractor:, reference_lister:, filename:, checkers:)
10
+ @reference_extractor = reference_extractor
11
+ @reference_lister = reference_lister
12
+ @filename = filename
13
+ @checkers = checkers
14
+ end
15
+
16
+ def call(node, ancestors:)
17
+ case Node.type(node)
18
+ when Node::METHOD_CALL, Node::CONSTANT
19
+ reference = @reference_extractor.reference_from_node(node, ancestors: ancestors, file_path: @filename)
20
+ check_reference(reference, node) if reference
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def check_reference(reference, node)
27
+ return nil unless (message = failed_check(reference))
28
+
29
+ constant = reference.constant
30
+
31
+ Packwerk::Offense.new(
32
+ location: Node.location(node),
33
+ file: @filename,
34
+ message: <<~EOS
35
+ #{message}
36
+ Inference details: this is a reference to #{constant.name} which seems to be defined in #{constant.location}.
37
+ To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations
38
+ EOS
39
+ )
40
+ end
41
+
42
+ def failed_check(reference)
43
+ failing_checker = @checkers.find do |checker|
44
+ checker.invalid_reference?(reference, @reference_lister)
45
+ end
46
+ failing_checker&.message_for(reference)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,22 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "packwerk/node"
5
+
6
+ module Packwerk
7
+ class NodeVisitor
8
+ def initialize(node_processor:)
9
+ @node_processor = node_processor
10
+ end
11
+
12
+ def visit(node, ancestors:, result:)
13
+ offense = @node_processor.call(node, ancestors: ancestors)
14
+ result << offense if offense
15
+
16
+ child_ancestors = [node] + ancestors
17
+ Node.each_child(node) do |child|
18
+ visit(child, ancestors: child_ancestors, result: result)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "parser/source/map"
5
+ require "sorbet-runtime"
6
+
7
+ require "packwerk/output_styles"
8
+
9
+ module Packwerk
10
+ class Offense
11
+ extend T::Sig
12
+ extend T::Helpers
13
+
14
+ attr_reader :location, :file, :message
15
+
16
+ sig do
17
+ params(file: String, message: String, location: T.nilable(Node::Location))
18
+ .void
19
+ end
20
+ def initialize(file:, message:, location: nil)
21
+ @location = location
22
+ @file = file
23
+ @message = message
24
+ end
25
+
26
+ sig do
27
+ params(style: T.any(T.class_of(OutputStyles::Plain), T.class_of(OutputStyles::Coloured)))
28
+ .returns(String)
29
+ end
30
+ def to_s(style = OutputStyles::Plain)
31
+ if location
32
+ <<~EOS
33
+ #{style.filename}#{file}#{style.reset}:#{location.line}:#{location.column}
34
+ #{@message}
35
+ EOS
36
+ else
37
+ <<~EOS
38
+ #{style.filename}#{file}#{style.reset}
39
+ #{@message}
40
+ EOS
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,41 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module OutputStyles
6
+ class Plain
7
+ class << self
8
+ def reset
9
+ ""
10
+ end
11
+
12
+ def filename
13
+ ""
14
+ end
15
+
16
+ def error
17
+ ""
18
+ end
19
+ end
20
+ end
21
+
22
+ # See https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit for ANSI escape colour codes
23
+ class Coloured
24
+ class << self
25
+ def reset
26
+ "\033[m"
27
+ end
28
+
29
+ def filename
30
+ # 36 is foreground cyan
31
+ "\033[36m"
32
+ end
33
+
34
+ def error
35
+ # 31 is foreground red
36
+ "\033[31m"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,56 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ class Package
6
+ include Comparable
7
+
8
+ ROOT_PACKAGE_NAME = "."
9
+
10
+ attr_reader :name, :dependencies
11
+
12
+ def initialize(name:, config:)
13
+ @name = name
14
+ @config = config || {}
15
+ @dependencies = Array(@config["dependencies"]).freeze
16
+ end
17
+
18
+ def enforce_privacy
19
+ @config["enforce_privacy"]
20
+ end
21
+
22
+ def enforce_dependencies?
23
+ @config["enforce_dependencies"] == true
24
+ end
25
+
26
+ def dependency?(package)
27
+ @dependencies.include?(package.name)
28
+ end
29
+
30
+ def package_path?(path)
31
+ return true if root?
32
+ path.start_with?(@name)
33
+ end
34
+
35
+ def public_path
36
+ @public_path ||= File.join(@name, "app/public/")
37
+ end
38
+
39
+ def public_path?(path)
40
+ path.start_with?(public_path)
41
+ end
42
+
43
+ def <=>(other)
44
+ return nil unless other.is_a?(self.class)
45
+ name <=> other.name
46
+ end
47
+
48
+ def to_s
49
+ name
50
+ end
51
+
52
+ def root?
53
+ @name == ROOT_PACKAGE_NAME
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,59 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "pathname"
5
+
6
+ require "packwerk/package"
7
+
8
+ module Packwerk
9
+ class PackageSet
10
+ include Enumerable
11
+
12
+ PACKAGE_CONFIG_FILENAME = "package.yml"
13
+
14
+ class << self
15
+ def load_all_from(root_path, package_pathspec: nil)
16
+ package_paths = package_paths(root_path, package_pathspec || "**")
17
+
18
+ packages = package_paths.map do |path|
19
+ root_relative = path.dirname.relative_path_from(root_path)
20
+ Package.new(name: root_relative.to_s, config: YAML.load_file(path))
21
+ end
22
+
23
+ create_root_package_if_none_in(packages)
24
+
25
+ new(packages)
26
+ end
27
+
28
+ private
29
+
30
+ def package_paths(root_path, package_pathspec)
31
+ Dir.glob(File.join(root_path, package_pathspec, PACKAGE_CONFIG_FILENAME))
32
+ .map! { |path| Pathname.new(path) }
33
+ end
34
+
35
+ def create_root_package_if_none_in(packages)
36
+ return if packages.any?(&:root?)
37
+ packages << Package.new(name: Package::ROOT_PACKAGE_NAME, config: nil)
38
+ end
39
+ end
40
+
41
+ def initialize(packages)
42
+ # We want to match more specific paths first
43
+ sorted_packages = packages.sort_by { |package| -package.name.length }
44
+ @packages = sorted_packages.each_with_object({}) { |package, hash| hash[package.name] = package }
45
+ end
46
+
47
+ def each(&blk)
48
+ @packages.values.each(&blk)
49
+ end
50
+
51
+ def fetch(name)
52
+ @packages[name]
53
+ end
54
+
55
+ def package_from_path(file_path)
56
+ @packages.values.find { |package| package.package_path?(file_path) }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,62 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "ast/node"
5
+
6
+ require "packwerk/node"
7
+
8
+ module Packwerk
9
+ class ParsedConstantDefinitions
10
+ def initialize(root_node:)
11
+ @local_definitions = {}
12
+
13
+ collect_local_definitions_from_root(root_node) if root_node
14
+ end
15
+
16
+ def local_reference?(constant_name, location: nil, namespace_path: [])
17
+ qualifications = self.class.reference_qualifications(constant_name, namespace_path: namespace_path)
18
+
19
+ qualifications.any? do |name|
20
+ @local_definitions[name] &&
21
+ @local_definitions[name] != location
22
+ end
23
+ end
24
+
25
+ # What fully qualified constants can this constant refer to in this context?
26
+ def self.reference_qualifications(constant_name, namespace_path:)
27
+ return [constant_name] if constant_name.start_with?("::")
28
+
29
+ fully_qualified_constant_name = "::#{constant_name}"
30
+
31
+ possible_namespaces = namespace_path.reduce([""]) do |acc, current|
32
+ acc << acc.last + "::" + current
33
+ end
34
+
35
+ possible_namespaces.map { |namespace| namespace + fully_qualified_constant_name }
36
+ end
37
+
38
+ private
39
+
40
+ def collect_local_definitions_from_root(node, current_namespace_path = [])
41
+ if Node.type(node) == Node::CONSTANT_ASSIGNMENT
42
+ add_definition(Node.constant_name(node), current_namespace_path, Node.name_location(node))
43
+ elsif Node.module_name_from_definition(node)
44
+ # handle compact constant nesting (e.g. "module Sales::Order")
45
+ tempnode = node
46
+ while (tempnode = Node.each_child(tempnode).find { |n| Node.type(n) == Node::CONSTANT })
47
+ add_definition(Node.constant_name(tempnode), current_namespace_path, Node.name_location(tempnode))
48
+ end
49
+
50
+ current_namespace_path += Node.class_or_module_name(node).split("::")
51
+ end
52
+
53
+ Node.each_child(node) { |child| collect_local_definitions_from_root(child, current_namespace_path) }
54
+ end
55
+
56
+ def add_definition(constant_name, current_namespace_path, location)
57
+ fully_qualified_constant = [""].concat(current_namespace_path).push(constant_name).join("::")
58
+
59
+ @local_definitions[fully_qualified_constant] = location
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,23 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "packwerk/offense"
5
+
6
+ module Packwerk
7
+ module Parsers
8
+ autoload :Erb, "packwerk/parsers/erb"
9
+ autoload :Factory, "packwerk/parsers/factory"
10
+ autoload :Ruby, "packwerk/parsers/ruby"
11
+
12
+ class ParseResult < Offense; end
13
+
14
+ class ParseError < StandardError
15
+ attr_reader :result
16
+
17
+ def initialize(result)
18
+ super(result.message)
19
+ @result = result
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,66 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "ast/node"
5
+ require "better_html"
6
+ require "better_html/parser"
7
+ require "parser/source/buffer"
8
+
9
+ require "packwerk/parsers"
10
+
11
+ module Packwerk
12
+ module Parsers
13
+ class Erb
14
+ def initialize(parser_class: BetterHtml::Parser, ruby_parser: Ruby.new)
15
+ @parser_class = parser_class
16
+ @ruby_parser = ruby_parser
17
+ end
18
+
19
+ def call(io:, file_path: "<unknown>")
20
+ buffer = Parser::Source::Buffer.new(file_path)
21
+ buffer.source = io.read
22
+ parser = @parser_class.new(buffer, template_language: :html)
23
+ to_ruby_ast(parser.ast, file_path)
24
+ rescue EncodingError => e
25
+ result = ParseResult.new(file: file_path, message: e.message)
26
+ raise Parsers::ParseError, result
27
+ rescue Parser::SyntaxError => e
28
+ result = ParseResult.new(file: file_path, message: "Syntax error: #{e}")
29
+ raise Parsers::ParseError, result
30
+ end
31
+
32
+ private
33
+
34
+ def to_ruby_ast(erb_ast, file_path)
35
+ # Note that we're not using the source location (line/column) at the moment, but if we did
36
+ # care about that, we'd need to tweak this to insert empty lines and spaces so that things
37
+ # line up with the ERB file
38
+ code_pieces = code_nodes(erb_ast).map do |node|
39
+ node.children.first
40
+ end
41
+
42
+ @ruby_parser.call(
43
+ io: StringIO.new(code_pieces.join("\n")),
44
+ file_path: file_path,
45
+ )
46
+ end
47
+
48
+ def code_nodes(node)
49
+ return enum_for(:code_nodes, node) unless block_given?
50
+ return unless node.is_a?(::AST::Node)
51
+
52
+ yield node if node.type == :code
53
+
54
+ # Skip descending into an ERB comment node, which may contain code nodes
55
+ if node.type == :erb
56
+ first_child = node.children.first
57
+ return if first_child&.type == :indicator && first_child&.children&.first == "#"
58
+ end
59
+
60
+ node.children.each do |child|
61
+ code_nodes(child) { |n| yield n }
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,34 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "singleton"
5
+
6
+ require "packwerk/parsers"
7
+
8
+ module Packwerk
9
+ module Parsers
10
+ class Factory
11
+ include Singleton
12
+
13
+ RUBY_REGEX = %r{
14
+ # Although not important for regex, these are ordered from most likely to match to least likely.
15
+ \.(rb|rake|builder|gemspec|ru)\Z
16
+ |
17
+ (Gemfile|Rakefile)\Z
18
+ }x
19
+ private_constant :RUBY_REGEX
20
+
21
+ ERB_REGEX = /\.erb\Z/
22
+ private_constant :ERB_REGEX
23
+
24
+ def for_path(path)
25
+ case path
26
+ when RUBY_REGEX
27
+ @ruby_parser ||= Ruby.new
28
+ when ERB_REGEX
29
+ @erb_parser ||= Erb.new
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end