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