packwerk 1.3.1 → 2.1.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 +4 -4
- data/CHANGELOG.md +1 -0
- data/Gemfile.lock +12 -9
- data/README.md +9 -3
- data/TROUBLESHOOT.md +3 -3
- data/UPGRADING.md +54 -0
- data/USAGE.md +32 -51
- data/exe/packwerk +7 -1
- data/lib/packwerk/application_load_paths.rb +1 -0
- data/lib/packwerk/application_validator.rb +17 -59
- data/lib/packwerk/association_inspector.rb +1 -1
- data/lib/packwerk/cache.rb +168 -0
- data/lib/packwerk/cli.rb +37 -20
- data/lib/packwerk/configuration.rb +40 -5
- data/lib/packwerk/const_node_inspector.rb +3 -2
- data/lib/packwerk/constant_discovery.rb +4 -4
- data/lib/packwerk/constant_name_inspector.rb +1 -1
- data/lib/packwerk/deprecated_references.rb +18 -6
- data/lib/packwerk/file_processor.rb +53 -14
- data/lib/packwerk/files_for_processing.rb +15 -4
- data/lib/packwerk/formatters/offenses_formatter.rb +1 -1
- data/lib/packwerk/formatters/progress_formatter.rb +1 -1
- data/lib/packwerk/generators/configuration_file.rb +4 -19
- data/lib/packwerk/generators/templates/package.yml +1 -1
- data/lib/packwerk/generators/templates/packwerk.yml.erb +5 -5
- data/lib/packwerk/graph.rb +2 -0
- data/lib/packwerk/node.rb +2 -0
- data/lib/packwerk/node_processor.rb +10 -22
- data/lib/packwerk/node_processor_factory.rb +0 -3
- data/lib/packwerk/node_visitor.rb +7 -2
- data/lib/packwerk/package.rb +25 -4
- data/lib/packwerk/package_set.rb +44 -8
- data/lib/packwerk/parsed_constant_definitions.rb +5 -4
- data/lib/packwerk/reference.rb +2 -1
- data/lib/packwerk/reference_checking/checkers/checker.rb +21 -0
- data/lib/packwerk/reference_checking/checkers/dependency_checker.rb +31 -0
- data/lib/packwerk/reference_checking/checkers/privacy_checker.rb +58 -0
- data/lib/packwerk/reference_checking/reference_checker.rb +33 -0
- data/lib/packwerk/reference_extractor.rb +66 -19
- data/lib/packwerk/reference_offense.rb +1 -0
- data/lib/packwerk/run_context.rb +32 -11
- data/lib/packwerk/sanity_checker.rb +1 -1
- data/lib/packwerk/spring_command.rb +28 -0
- data/lib/packwerk/unresolved_reference.rb +10 -0
- data/lib/packwerk/version.rb +1 -1
- data/lib/packwerk/violation_type.rb +1 -1
- data/lib/packwerk.rb +19 -12
- data/packwerk.gemspec +4 -1
- data/service.yml +0 -2
- data/sorbet/rbi/gems/psych@3.3.2.rbi +24 -0
- metadata +41 -11
- data/lib/packwerk/checker.rb +0 -17
- data/lib/packwerk/dependency_checker.rb +0 -26
- data/lib/packwerk/generators/inflections_file.rb +0 -43
- data/lib/packwerk/generators/templates/inflections.yml +0 -6
- data/lib/packwerk/inflections/custom.rb +0 -33
- data/lib/packwerk/inflections/default.rb +0 -73
- data/lib/packwerk/inflector.rb +0 -48
- data/lib/packwerk/privacy_checker.rb +0 -53
@@ -12,12 +12,12 @@
|
|
12
12
|
# Patterns to find package configuration files
|
13
13
|
# package_paths: "**/"
|
14
14
|
|
15
|
-
# List of application load paths
|
16
|
-
<%= @load_paths_comment -%>
|
17
|
-
<%= @load_paths_formatted %>
|
18
15
|
# List of custom associations, if any
|
19
16
|
# custom_associations:
|
20
17
|
# - "cache_belongs_to"
|
21
18
|
|
22
|
-
#
|
23
|
-
#
|
19
|
+
# Whether or not you want the cache enabled (disabled by default)
|
20
|
+
# cache: true
|
21
|
+
|
22
|
+
# Where you want the cache to be stored (default below)
|
23
|
+
# cache_directory: 'tmp/cache/packwerk'
|
data/lib/packwerk/graph.rb
CHANGED
@@ -2,7 +2,9 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module Packwerk
|
5
|
+
# A general implementation of a graph data structure with the ability to check for - and list - cycles.
|
5
6
|
class Graph
|
7
|
+
# @param [Array<Array>] edges The edges of the graph; An edge being represented as an Array of two nodes.
|
6
8
|
def initialize(*edges)
|
7
9
|
@edges = edges.uniq
|
8
10
|
@cycles = Set.new
|
data/lib/packwerk/node.rb
CHANGED
@@ -5,6 +5,7 @@ require "parser"
|
|
5
5
|
require "parser/ast/node"
|
6
6
|
|
7
7
|
module Packwerk
|
8
|
+
# Convenience methods for working with Parser::AST::Node nodes.
|
8
9
|
module Node
|
9
10
|
class TypeError < ArgumentError; end
|
10
11
|
Location = Struct.new(:line, :column)
|
@@ -263,6 +264,7 @@ module Packwerk
|
|
263
264
|
# "Class.new"
|
264
265
|
# "Module.new"
|
265
266
|
method_call?(node) &&
|
267
|
+
receiver(node) &&
|
266
268
|
constant?(receiver(node)) &&
|
267
269
|
["Class", "Module"].include?(constant_name(receiver(node))) &&
|
268
270
|
method_name(node) == :new
|
@@ -2,6 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module Packwerk
|
5
|
+
# Processes a single node in an abstract syntax tree (AST) using the provided checkers.
|
5
6
|
class NodeProcessor
|
6
7
|
extend T::Sig
|
7
8
|
|
@@ -9,35 +10,22 @@ module Packwerk
|
|
9
10
|
params(
|
10
11
|
reference_extractor: ReferenceExtractor,
|
11
12
|
filename: String,
|
12
|
-
checkers: T::Array[Checker]
|
13
13
|
).void
|
14
14
|
end
|
15
|
-
def initialize(reference_extractor:, filename
|
15
|
+
def initialize(reference_extractor:, filename:)
|
16
16
|
@reference_extractor = reference_extractor
|
17
17
|
@filename = filename
|
18
|
-
@checkers = checkers
|
19
18
|
end
|
20
19
|
|
21
|
-
sig
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
20
|
+
sig do
|
21
|
+
params(
|
22
|
+
node: Parser::AST::Node,
|
23
|
+
ancestors: T::Array[Parser::AST::Node]
|
24
|
+
).returns(T.nilable(UnresolvedReference))
|
26
25
|
end
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def check_reference(reference, node)
|
31
|
-
return [] unless reference
|
32
|
-
@checkers.each_with_object([]) do |checker, violations|
|
33
|
-
next unless checker.invalid_reference?(reference)
|
34
|
-
offense = Packwerk::ReferenceOffense.new(
|
35
|
-
location: Node.location(node),
|
36
|
-
reference: reference,
|
37
|
-
violation_type: checker.violation_type
|
38
|
-
)
|
39
|
-
violations << offense
|
40
|
-
end
|
26
|
+
def call(node, ancestors)
|
27
|
+
return unless Node.method_call?(node) || Node.constant?(node)
|
28
|
+
@reference_extractor.reference_from_node(node, ancestors: ancestors, file_path: @filename)
|
41
29
|
end
|
42
30
|
end
|
43
31
|
end
|
@@ -8,14 +8,12 @@ module Packwerk
|
|
8
8
|
const :root_path, String
|
9
9
|
const :context_provider, Packwerk::ConstantDiscovery
|
10
10
|
const :constant_name_inspectors, T::Array[ConstantNameInspector]
|
11
|
-
const :checkers, T::Array[Checker]
|
12
11
|
|
13
12
|
sig { params(filename: String, node: AST::Node).returns(NodeProcessor) }
|
14
13
|
def for(filename:, node:)
|
15
14
|
::Packwerk::NodeProcessor.new(
|
16
15
|
reference_extractor: reference_extractor(node: node),
|
17
16
|
filename: filename,
|
18
|
-
checkers: checkers,
|
19
17
|
)
|
20
18
|
end
|
21
19
|
|
@@ -24,7 +22,6 @@ module Packwerk
|
|
24
22
|
sig { params(node: AST::Node).returns(::Packwerk::ReferenceExtractor) }
|
25
23
|
def reference_extractor(node:)
|
26
24
|
::Packwerk::ReferenceExtractor.new(
|
27
|
-
context_provider: context_provider,
|
28
25
|
constant_name_inspectors: constant_name_inspectors,
|
29
26
|
root_node: node,
|
30
27
|
root_path: root_path,
|
@@ -1,14 +1,19 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: true
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module Packwerk
|
5
|
+
# Visits all nodes of an AST, processing them using a given node processor.
|
5
6
|
class NodeVisitor
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { params(node_processor: NodeProcessor).void }
|
6
10
|
def initialize(node_processor:)
|
7
11
|
@node_processor = node_processor
|
8
12
|
end
|
9
13
|
|
10
14
|
def visit(node, ancestors:, result:)
|
11
|
-
|
15
|
+
reference = @node_processor.call(node, ancestors)
|
16
|
+
result << reference if reference
|
12
17
|
|
13
18
|
child_ancestors = [node] + ancestors
|
14
19
|
Node.each_child(node) do |child|
|
data/lib/packwerk/package.rb
CHANGED
@@ -1,37 +1,51 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module Packwerk
|
5
|
+
# The basic unit of modularity for packwerk; a folder that has been declared to define a package.
|
6
|
+
# The package contains all constants defined in files in this folder and all subfolders that are not packages
|
7
|
+
# themselves.
|
5
8
|
class Package
|
9
|
+
extend T::Sig
|
6
10
|
include Comparable
|
7
11
|
|
8
12
|
ROOT_PACKAGE_NAME = "."
|
9
13
|
|
10
|
-
|
14
|
+
sig { returns(String) }
|
15
|
+
attr_reader :name
|
16
|
+
sig { returns(T::Array[String]) }
|
17
|
+
attr_reader :dependencies
|
11
18
|
|
19
|
+
sig { params(name: String, config: T.nilable(T.any(T::Hash[T.untyped, T.untyped], FalseClass))).void }
|
12
20
|
def initialize(name:, config:)
|
13
21
|
@name = name
|
14
|
-
@config = config || {}
|
15
|
-
@dependencies = Array(@config["dependencies"]).freeze
|
22
|
+
@config = T.let(config || {}, T::Hash[T.untyped, T.untyped])
|
23
|
+
@dependencies = T.let(Array(@config["dependencies"]).freeze, T::Array[String])
|
24
|
+
@public_path = T.let(nil, T.nilable(String))
|
16
25
|
end
|
17
26
|
|
27
|
+
sig { returns(T.nilable(T.any(T::Boolean, T::Array[String]))) }
|
18
28
|
def enforce_privacy
|
19
29
|
@config["enforce_privacy"]
|
20
30
|
end
|
21
31
|
|
32
|
+
sig { returns(T::Boolean) }
|
22
33
|
def enforce_dependencies?
|
23
34
|
@config["enforce_dependencies"] == true
|
24
35
|
end
|
25
36
|
|
37
|
+
sig { params(package: Package).returns(T::Boolean) }
|
26
38
|
def dependency?(package)
|
27
39
|
@dependencies.include?(package.name)
|
28
40
|
end
|
29
41
|
|
42
|
+
sig { params(path: String).returns(T::Boolean) }
|
30
43
|
def package_path?(path)
|
31
44
|
return true if root?
|
32
45
|
path.start_with?(@name)
|
33
46
|
end
|
34
47
|
|
48
|
+
sig { returns(String) }
|
35
49
|
def public_path
|
36
50
|
@public_path ||= begin
|
37
51
|
unprefixed_public_path = user_defined_public_path || "app/public/"
|
@@ -44,10 +58,12 @@ module Packwerk
|
|
44
58
|
end
|
45
59
|
end
|
46
60
|
|
61
|
+
sig { params(path: String).returns(T::Boolean) }
|
47
62
|
def public_path?(path)
|
48
63
|
path.start_with?(public_path)
|
49
64
|
end
|
50
65
|
|
66
|
+
sig { returns(T.nilable(String)) }
|
51
67
|
def user_defined_public_path
|
52
68
|
return unless @config["public_path"]
|
53
69
|
return @config["public_path"] if @config["public_path"].end_with?("/")
|
@@ -55,23 +71,28 @@ module Packwerk
|
|
55
71
|
@config["public_path"] + "/"
|
56
72
|
end
|
57
73
|
|
74
|
+
sig { params(other: T.untyped).returns(T.nilable(Integer)) }
|
58
75
|
def <=>(other)
|
59
76
|
return nil unless other.is_a?(self.class)
|
60
77
|
name <=> other.name
|
61
78
|
end
|
62
79
|
|
80
|
+
sig { params(other: T.untyped).returns(T::Boolean) }
|
63
81
|
def eql?(other)
|
64
82
|
self == other
|
65
83
|
end
|
66
84
|
|
85
|
+
sig { returns(Integer) }
|
67
86
|
def hash
|
68
87
|
name.hash
|
69
88
|
end
|
70
89
|
|
90
|
+
sig { returns(String) }
|
71
91
|
def to_s
|
72
92
|
name
|
73
93
|
end
|
74
94
|
|
95
|
+
sig { returns(T::Boolean) }
|
75
96
|
def root?
|
76
97
|
@name == ROOT_PACKAGE_NAME
|
77
98
|
end
|
data/lib/packwerk/package_set.rb
CHANGED
@@ -1,15 +1,25 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "pathname"
|
5
5
|
|
6
6
|
module Packwerk
|
7
|
+
PathSpec = T.type_alias { T.any(String, T::Array[String]) }
|
8
|
+
|
9
|
+
# A set of {Packwerk::Package}s as well as methods to parse packages from the filesystem.
|
7
10
|
class PackageSet
|
11
|
+
extend T::Sig
|
12
|
+
extend T::Generic
|
8
13
|
include Enumerable
|
9
14
|
|
15
|
+
Elem = type_member(fixed: Package)
|
16
|
+
|
10
17
|
PACKAGE_CONFIG_FILENAME = "package.yml"
|
11
18
|
|
12
19
|
class << self
|
20
|
+
extend T::Sig
|
21
|
+
|
22
|
+
sig { params(root_path: String, package_pathspec: T.nilable(PathSpec)).returns(PackageSet) }
|
13
23
|
def load_all_from(root_path, package_pathspec: nil)
|
14
24
|
package_paths = package_paths(root_path, package_pathspec || "**")
|
15
25
|
|
@@ -23,8 +33,17 @@ module Packwerk
|
|
23
33
|
new(packages)
|
24
34
|
end
|
25
35
|
|
26
|
-
|
27
|
-
|
36
|
+
sig do
|
37
|
+
params(
|
38
|
+
root_path: String,
|
39
|
+
package_pathspec: PathSpec,
|
40
|
+
exclude_pathspec: T.nilable(PathSpec)
|
41
|
+
).returns(T::Array[Pathname])
|
42
|
+
end
|
43
|
+
def package_paths(root_path, package_pathspec, exclude_pathspec = [])
|
44
|
+
exclude_pathspec = Array(exclude_pathspec).dup
|
45
|
+
.push(Bundler.bundle_path.join("**").to_s)
|
46
|
+
.map { |glob| File.expand_path(glob) }
|
28
47
|
|
29
48
|
glob_patterns = Array(package_pathspec).map do |pathspec|
|
30
49
|
File.join(root_path, pathspec, PACKAGE_CONFIG_FILENAME)
|
@@ -32,34 +51,51 @@ module Packwerk
|
|
32
51
|
|
33
52
|
Dir.glob(glob_patterns)
|
34
53
|
.map { |path| Pathname.new(path).cleanpath }
|
35
|
-
.reject { |path| path
|
54
|
+
.reject { |path| exclude_path?(exclude_pathspec, path) }
|
36
55
|
end
|
37
56
|
|
38
57
|
private
|
39
58
|
|
59
|
+
sig { params(packages: T::Array[Package]).void }
|
40
60
|
def create_root_package_if_none_in(packages)
|
41
61
|
return if packages.any?(&:root?)
|
42
62
|
packages << Package.new(name: Package::ROOT_PACKAGE_NAME, config: nil)
|
43
63
|
end
|
64
|
+
|
65
|
+
sig { params(globs: T::Array[String], path: Pathname).returns(T::Boolean) }
|
66
|
+
def exclude_path?(globs, path)
|
67
|
+
globs.any? do |glob|
|
68
|
+
path.realpath.fnmatch(glob, File::FNM_EXTGLOB)
|
69
|
+
end
|
70
|
+
end
|
44
71
|
end
|
45
72
|
|
73
|
+
sig { returns(T::Hash[String, Package]) }
|
74
|
+
attr_reader :packages
|
75
|
+
|
76
|
+
sig { params(packages: T::Array[Package]).void }
|
46
77
|
def initialize(packages)
|
47
78
|
# We want to match more specific paths first
|
48
79
|
sorted_packages = packages.sort_by { |package| -package.name.length }
|
49
|
-
|
80
|
+
packages = sorted_packages.each_with_object({}) { |package, hash| hash[package.name] = package }
|
81
|
+
@packages = T.let(packages, T::Hash[String, Package])
|
82
|
+
@package_from_path = T.let({}, T::Hash[String, T.nilable(Package)])
|
50
83
|
end
|
51
84
|
|
85
|
+
sig { override.params(blk: T.proc.params(arg0: Package).returns(T.untyped)).returns(T.untyped) }
|
52
86
|
def each(&blk)
|
53
|
-
|
87
|
+
packages.values.each(&blk)
|
54
88
|
end
|
55
89
|
|
90
|
+
sig { params(name: String).returns(T.nilable(Package)) }
|
56
91
|
def fetch(name)
|
57
|
-
|
92
|
+
packages[name]
|
58
93
|
end
|
59
94
|
|
95
|
+
sig { params(file_path: T.any(Pathname, String)).returns(T.nilable(Package)) }
|
60
96
|
def package_from_path(file_path)
|
61
97
|
path_string = file_path.to_s
|
62
|
-
@packages.values.find { |package| package.package_path?(path_string) }
|
98
|
+
@package_from_path[path_string] ||= packages.values.find { |package| package.package_path?(path_string) }
|
63
99
|
end
|
64
100
|
end
|
65
101
|
end
|
@@ -4,6 +4,7 @@
|
|
4
4
|
require "ast/node"
|
5
5
|
|
6
6
|
module Packwerk
|
7
|
+
# A collection of constant definitions parsed from an Abstract Syntax Tree (AST).
|
7
8
|
class ParsedConstantDefinitions
|
8
9
|
def initialize(root_node:)
|
9
10
|
@local_definitions = {}
|
@@ -24,13 +25,13 @@ module Packwerk
|
|
24
25
|
def self.reference_qualifications(constant_name, namespace_path:)
|
25
26
|
return [constant_name] if constant_name.start_with?("::")
|
26
27
|
|
27
|
-
|
28
|
+
resolved_constant_name = "::#{constant_name}"
|
28
29
|
|
29
30
|
possible_namespaces = namespace_path.each_with_object([""]) do |current, acc|
|
30
31
|
acc << "#{acc.last}::#{current}" if acc.last && current
|
31
32
|
end
|
32
33
|
|
33
|
-
possible_namespaces.map { |namespace| namespace +
|
34
|
+
possible_namespaces.map { |namespace| namespace + resolved_constant_name }
|
34
35
|
end
|
35
36
|
|
36
37
|
private
|
@@ -52,9 +53,9 @@ module Packwerk
|
|
52
53
|
end
|
53
54
|
|
54
55
|
def add_definition(constant_name, current_namespace_path, location)
|
55
|
-
|
56
|
+
resolved_constant = [""].concat(current_namespace_path).push(constant_name).join("::")
|
56
57
|
|
57
|
-
@local_definitions[
|
58
|
+
@local_definitions[resolved_constant] = location
|
58
59
|
end
|
59
60
|
end
|
60
61
|
end
|
data/lib/packwerk/reference.rb
CHANGED
@@ -2,5 +2,6 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module Packwerk
|
5
|
-
|
5
|
+
# A reference from a file in one package to a constant that may be defined in a different package.
|
6
|
+
Reference = Struct.new(:source_package, :relative_path, :constant, :source_location)
|
6
7
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Packwerk
|
5
|
+
module ReferenceChecking
|
6
|
+
module Checkers
|
7
|
+
module Checker
|
8
|
+
extend T::Sig
|
9
|
+
extend T::Helpers
|
10
|
+
|
11
|
+
interface!
|
12
|
+
|
13
|
+
sig { returns(ViolationType).abstract }
|
14
|
+
def violation_type; end
|
15
|
+
|
16
|
+
sig { params(reference: Reference).returns(T::Boolean).abstract }
|
17
|
+
def invalid_reference?(reference); end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Packwerk
|
5
|
+
module ReferenceChecking
|
6
|
+
module Checkers
|
7
|
+
# Checks whether a given reference conforms to the configured graph of dependencies.
|
8
|
+
class DependencyChecker
|
9
|
+
extend T::Sig
|
10
|
+
include Checker
|
11
|
+
|
12
|
+
sig { override.returns(ViolationType) }
|
13
|
+
def violation_type
|
14
|
+
ViolationType::Dependency
|
15
|
+
end
|
16
|
+
|
17
|
+
sig do
|
18
|
+
override
|
19
|
+
.params(reference: Packwerk::Reference)
|
20
|
+
.returns(T::Boolean)
|
21
|
+
end
|
22
|
+
def invalid_reference?(reference)
|
23
|
+
return false unless reference.source_package
|
24
|
+
return false unless reference.source_package.enforce_dependencies?
|
25
|
+
return false if reference.source_package.dependency?(reference.constant.package)
|
26
|
+
true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Packwerk
|
5
|
+
module ReferenceChecking
|
6
|
+
module Checkers
|
7
|
+
# Checks whether a given reference references a private constant of another package.
|
8
|
+
class PrivacyChecker
|
9
|
+
extend T::Sig
|
10
|
+
include Checker
|
11
|
+
|
12
|
+
sig { override.returns(Packwerk::ViolationType) }
|
13
|
+
def violation_type
|
14
|
+
ViolationType::Privacy
|
15
|
+
end
|
16
|
+
|
17
|
+
sig do
|
18
|
+
override
|
19
|
+
.params(reference: Packwerk::Reference)
|
20
|
+
.returns(T::Boolean)
|
21
|
+
end
|
22
|
+
def invalid_reference?(reference)
|
23
|
+
return false if reference.constant.public?
|
24
|
+
|
25
|
+
privacy_option = reference.constant.package.enforce_privacy
|
26
|
+
return false if enforcement_disabled?(privacy_option)
|
27
|
+
|
28
|
+
return false unless privacy_option == true ||
|
29
|
+
explicitly_private_constant?(reference.constant, explicitly_private_constants: privacy_option)
|
30
|
+
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
sig do
|
37
|
+
params(
|
38
|
+
constant: ConstantDiscovery::ConstantContext,
|
39
|
+
explicitly_private_constants: T::Array[String]
|
40
|
+
).returns(T::Boolean)
|
41
|
+
end
|
42
|
+
def explicitly_private_constant?(constant, explicitly_private_constants:)
|
43
|
+
explicitly_private_constants.include?(constant.name) ||
|
44
|
+
# nested constants
|
45
|
+
explicitly_private_constants.any? { |epc| constant.name.start_with?(epc + "::") }
|
46
|
+
end
|
47
|
+
|
48
|
+
sig do
|
49
|
+
params(privacy_option: T.nilable(T.any(T::Boolean, T::Array[String])))
|
50
|
+
.returns(T::Boolean)
|
51
|
+
end
|
52
|
+
def enforcement_disabled?(privacy_option)
|
53
|
+
[false, nil].include?(privacy_option)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Packwerk
|
5
|
+
module ReferenceChecking
|
6
|
+
class ReferenceChecker
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
def initialize(checkers)
|
10
|
+
@checkers = checkers
|
11
|
+
end
|
12
|
+
|
13
|
+
sig do
|
14
|
+
params(
|
15
|
+
reference: T.any(Packwerk::Reference, Packwerk::Offense)
|
16
|
+
).returns(T::Array[Packwerk::Offense])
|
17
|
+
end
|
18
|
+
def call(reference)
|
19
|
+
return [reference] if reference.is_a?(Packwerk::Offense)
|
20
|
+
|
21
|
+
@checkers.each_with_object([]) do |checker, violations|
|
22
|
+
next unless checker.invalid_reference?(reference)
|
23
|
+
offense = Packwerk::ReferenceOffense.new(
|
24
|
+
location: reference.source_location,
|
25
|
+
reference: reference,
|
26
|
+
violation_type: checker.violation_type
|
27
|
+
)
|
28
|
+
violations << offense
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -2,64 +2,111 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module Packwerk
|
5
|
-
#
|
5
|
+
# Extracts a possible constant reference from a given AST node.
|
6
6
|
class ReferenceExtractor
|
7
7
|
extend T::Sig
|
8
8
|
|
9
9
|
sig do
|
10
10
|
params(
|
11
|
-
context_provider: Packwerk::ConstantDiscovery,
|
12
11
|
constant_name_inspectors: T::Array[Packwerk::ConstantNameInspector],
|
13
12
|
root_node: ::AST::Node,
|
14
13
|
root_path: String,
|
15
14
|
).void
|
16
15
|
end
|
17
16
|
def initialize(
|
18
|
-
context_provider:,
|
19
17
|
constant_name_inspectors:,
|
20
18
|
root_node:,
|
21
19
|
root_path:
|
22
20
|
)
|
23
|
-
@context_provider = context_provider
|
24
21
|
@constant_name_inspectors = constant_name_inspectors
|
25
22
|
@root_path = root_path
|
26
23
|
@local_constant_definitions = ParsedConstantDefinitions.new(root_node: root_node)
|
27
24
|
end
|
28
25
|
|
26
|
+
sig do
|
27
|
+
params(
|
28
|
+
node: Parser::AST::Node,
|
29
|
+
ancestors: T::Array[Parser::AST::Node],
|
30
|
+
file_path: String
|
31
|
+
).returns(T.nilable(UnresolvedReference))
|
32
|
+
end
|
29
33
|
def reference_from_node(node, ancestors:, file_path:)
|
30
34
|
constant_name = T.let(nil, T.nilable(String))
|
31
35
|
|
32
36
|
@constant_name_inspectors.each do |inspector|
|
33
37
|
constant_name = inspector.constant_name_from_node(node, ancestors: ancestors)
|
38
|
+
|
34
39
|
break if constant_name
|
35
40
|
end
|
36
41
|
|
37
42
|
reference_from_constant(constant_name, node: node, ancestors: ancestors, file_path: file_path) if constant_name
|
38
43
|
end
|
39
44
|
|
40
|
-
|
45
|
+
sig do
|
46
|
+
params(
|
47
|
+
unresolved_references_and_offenses: T::Array[T.any(UnresolvedReference, Offense)],
|
48
|
+
context_provider: ConstantDiscovery
|
49
|
+
).returns(T::Array[T.any(Reference, Offense)])
|
50
|
+
end
|
51
|
+
def self.get_fully_qualified_references_and_offenses_from(unresolved_references_and_offenses, context_provider)
|
52
|
+
fully_qualified_references_and_offenses = T.let([], T::Array[T.any(Reference, Offense)])
|
41
53
|
|
42
|
-
|
43
|
-
|
44
|
-
|
54
|
+
unresolved_references_and_offenses.each do |unresolved_references_or_offense|
|
55
|
+
if unresolved_references_or_offense.is_a?(Offense)
|
56
|
+
fully_qualified_references_and_offenses << unresolved_references_or_offense
|
57
|
+
|
58
|
+
next
|
59
|
+
end
|
60
|
+
|
61
|
+
unresolved_reference = unresolved_references_or_offense
|
62
|
+
|
63
|
+
constant =
|
64
|
+
context_provider.context_for(
|
65
|
+
unresolved_reference.constant_name,
|
66
|
+
current_namespace_path: unresolved_reference.namespace_path
|
67
|
+
)
|
68
|
+
|
69
|
+
next if constant&.package.nil?
|
45
70
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
71
|
+
source_package = context_provider.package_from_path(unresolved_reference.relative_path)
|
72
|
+
|
73
|
+
next if source_package == constant.package
|
74
|
+
|
75
|
+
fully_qualified_references_and_offenses << Reference.new(
|
76
|
+
source_package,
|
77
|
+
unresolved_reference.relative_path,
|
78
|
+
constant,
|
79
|
+
unresolved_reference.source_location
|
50
80
|
)
|
81
|
+
end
|
51
82
|
|
52
|
-
|
83
|
+
fully_qualified_references_and_offenses
|
84
|
+
end
|
53
85
|
|
54
|
-
|
55
|
-
|
56
|
-
|
86
|
+
private
|
87
|
+
|
88
|
+
sig do
|
89
|
+
params(
|
90
|
+
constant_name: String,
|
91
|
+
node: Parser::AST::Node,
|
92
|
+
ancestors: T::Array[Parser::AST::Node],
|
93
|
+
file_path: String
|
94
|
+
).returns(T.nilable(UnresolvedReference))
|
95
|
+
end
|
96
|
+
def reference_from_constant(constant_name, node:, ancestors:, file_path:)
|
97
|
+
namespace_path = Node.enclosing_namespace_path(node, ancestors: ancestors)
|
57
98
|
|
58
|
-
|
99
|
+
return if local_reference?(constant_name, Node.name_location(node), namespace_path)
|
59
100
|
|
60
|
-
|
101
|
+
relative_path = Pathname.new(file_path).relative_path_from(@root_path).to_s
|
102
|
+
location = Node.location(node)
|
61
103
|
|
62
|
-
|
104
|
+
UnresolvedReference.new(
|
105
|
+
constant_name,
|
106
|
+
namespace_path,
|
107
|
+
relative_path,
|
108
|
+
location
|
109
|
+
)
|
63
110
|
end
|
64
111
|
|
65
112
|
def local_reference?(constant_name, name_location, namespace_path)
|