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