packwerk 1.0.0 → 1.1.2

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/pull_request_template.md +8 -7
  3. data/.github/workflows/ci.yml +1 -1
  4. data/.gitignore +1 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +5 -2
  7. data/README.md +5 -3
  8. data/TROUBLESHOOT.md +1 -1
  9. data/USAGE.md +56 -19
  10. data/exe/packwerk +1 -1
  11. data/lib/packwerk.rb +3 -3
  12. data/lib/packwerk/application_load_paths.rb +68 -0
  13. data/lib/packwerk/application_validator.rb +96 -70
  14. data/lib/packwerk/association_inspector.rb +50 -20
  15. data/lib/packwerk/cache_deprecated_references.rb +55 -0
  16. data/lib/packwerk/checker.rb +23 -0
  17. data/lib/packwerk/checking_deprecated_references.rb +5 -2
  18. data/lib/packwerk/cli.rb +65 -56
  19. data/lib/packwerk/commands/detect_stale_violations_command.rb +60 -0
  20. data/lib/packwerk/commands/offense_progress_marker.rb +24 -0
  21. data/lib/packwerk/commands/result.rb +13 -0
  22. data/lib/packwerk/commands/update_deprecations_command.rb +81 -0
  23. data/lib/packwerk/configuration.rb +6 -34
  24. data/lib/packwerk/const_node_inspector.rb +28 -17
  25. data/lib/packwerk/dependency_checker.rb +16 -5
  26. data/lib/packwerk/deprecated_references.rb +24 -1
  27. data/lib/packwerk/detect_stale_deprecated_references.rb +14 -0
  28. data/lib/packwerk/file_processor.rb +4 -4
  29. data/lib/packwerk/formatters/offenses_formatter.rb +48 -0
  30. data/lib/packwerk/formatters/progress_formatter.rb +6 -2
  31. data/lib/packwerk/generators/application_validation.rb +2 -2
  32. data/lib/packwerk/generators/templates/package.yml +4 -0
  33. data/lib/packwerk/generators/templates/packwerk +2 -2
  34. data/lib/packwerk/generators/templates/packwerk.yml.erb +1 -1
  35. data/lib/packwerk/inflector.rb +17 -8
  36. data/lib/packwerk/node.rb +78 -39
  37. data/lib/packwerk/node_processor.rb +14 -3
  38. data/lib/packwerk/node_processor_factory.rb +39 -0
  39. data/lib/packwerk/offense.rb +4 -6
  40. data/lib/packwerk/output_style.rb +20 -0
  41. data/lib/packwerk/output_styles/coloured.rb +29 -0
  42. data/lib/packwerk/output_styles/plain.rb +26 -0
  43. data/lib/packwerk/package.rb +8 -1
  44. data/lib/packwerk/package_set.rb +13 -5
  45. data/lib/packwerk/parsed_constant_definitions.rb +4 -4
  46. data/lib/packwerk/parsers/erb.rb +4 -0
  47. data/lib/packwerk/parsers/factory.rb +10 -1
  48. data/lib/packwerk/privacy_checker.rb +26 -5
  49. data/lib/packwerk/run_context.rb +70 -46
  50. data/lib/packwerk/sanity_checker.rb +1 -1
  51. data/lib/packwerk/spring_command.rb +1 -1
  52. data/lib/packwerk/updating_deprecated_references.rb +2 -39
  53. data/lib/packwerk/version.rb +1 -1
  54. data/packwerk.gemspec +2 -2
  55. metadata +15 -8
  56. data/lib/packwerk/output_styles.rb +0 -41
  57. data/static/packwerk-check-demo.png +0 -0
  58. data/static/packwerk_check.gif +0 -0
  59. data/static/packwerk_check_violation.gif +0 -0
  60. data/static/packwerk_update.gif +0 -0
  61. data/static/packwerk_validate.gif +0 -0
@@ -35,7 +35,7 @@ module Packwerk
35
35
  return true
36
36
  end
37
37
 
38
- source_file_path = File.expand_path("../templates/packwerk", __FILE__)
38
+ source_file_path = File.expand_path("templates/packwerk", __dir__)
39
39
  FileUtils.cp(source_file_path, destination_file_path)
40
40
 
41
41
  @out.puts("✅ Packwerk application validation bin script generated in #{destination_file_path}")
@@ -51,7 +51,7 @@ module Packwerk
51
51
  return true
52
52
  end
53
53
 
54
- source_file_path = File.expand_path("../templates/packwerk_validator_test.rb", __FILE__)
54
+ source_file_path = File.expand_path("templates/packwerk_validator_test.rb", __dir__)
55
55
  FileUtils.cp(source_file_path, destination_file_path)
56
56
 
57
57
  @out.puts("✅ Packwerk application validation test generated in #{destination_file_path}")
@@ -11,6 +11,10 @@ enforce_dependencies: true
11
11
  # We recommend enabling this for any new packages you create to aid with encapsulation.
12
12
  enforce_privacy: false
13
13
 
14
+ # By default the public path will be app/public/, however this may not suit all applications' architecture so
15
+ # this allows you to modify what your package's public path is.
16
+ # public_path: app/public/
17
+
14
18
  # A list of this package's dependencies
15
19
  # Note that packages in this list require their own `package.yml` file
16
20
  # dependencies:
@@ -10,12 +10,12 @@ ENV["RAILS_ENV"] = "test"
10
10
  packwerk_argv = ARGV.dup
11
11
 
12
12
  begin
13
- load(File.expand_path("../spring", __FILE__))
13
+ load(File.expand_path("spring", __dir__))
14
14
  rescue LoadError => e
15
15
  raise unless e.message.include?("spring")
16
16
  end
17
17
 
18
- require File.expand_path("../../config/environment", __FILE__)
18
+ require File.expand_path("../config/environment", __dir__)
19
19
 
20
20
  require "packwerk"
21
21
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  # List of patterns for folder paths to exclude
9
9
  # exclude:
10
- # - "{bin,node_modules,script,tmp}/**/*"
10
+ # - "{bin,node_modules,script,tmp,vendor}/**/*"
11
11
 
12
12
  # Patterns to find package configuration files
13
13
  # package_paths: "**/"
@@ -8,20 +8,31 @@ require "packwerk/inflections/custom"
8
8
  module Packwerk
9
9
  class Inflector
10
10
  class << self
11
+ extend T::Sig
12
+
11
13
  def default
12
- @default ||= new
14
+ @default ||= new(custom_inflector: Inflections::Custom.new)
15
+ end
16
+
17
+ sig { params(inflections_file: String).returns(::Packwerk::Inflector) }
18
+ def from_file(inflections_file)
19
+ new(custom_inflector: Inflections::Custom.new(inflections_file))
13
20
  end
14
21
  end
15
22
 
16
- # For #camelize, #classify, #pluralize, #singularize
17
- include ::ActiveSupport::Inflector
23
+ extend T::Sig
24
+ include ::ActiveSupport::Inflector # For #camelize, #classify, #pluralize, #singularize
18
25
 
19
- def initialize(custom_inflection_file: nil)
26
+ sig do
27
+ params(
28
+ custom_inflector: Inflections::Custom
29
+ ).void
30
+ end
31
+ def initialize(custom_inflector:)
20
32
  @inflections = ::ActiveSupport::Inflector::Inflections.new
21
33
 
22
34
  Inflections::Default.apply_to(@inflections)
23
-
24
- Inflections::Custom.new(custom_inflection_file).apply_to(@inflections)
35
+ custom_inflector.apply_to(@inflections)
25
36
  end
26
37
 
27
38
  def pluralize(word, count = nil)
@@ -32,8 +43,6 @@ module Packwerk
32
43
  end
33
44
  end
34
45
 
35
- private
36
-
37
46
  def inflections(_ = nil)
38
47
  @inflections
39
48
  end
@@ -5,24 +5,14 @@ require "parser/ast/node"
5
5
 
6
6
  module Packwerk
7
7
  module Node
8
- BLOCK = :block
9
- CLASS = :class
10
- CONSTANT = :const
11
- CONSTANT_ASSIGNMENT = :casgn
12
- CONSTANT_ROOT_NAMESPACE = :cbase
13
- HASH = :hash
14
- HASH_PAIR = :pair
15
- METHOD_CALL = :send
16
- MODULE = :module
17
- STRING = :str
18
- SYMBOL = :sym
19
-
20
8
  class TypeError < ArgumentError; end
21
9
  Location = Struct.new(:line, :column)
22
10
 
23
11
  class << self
12
+ extend T::Sig
13
+
24
14
  def class_or_module_name(class_or_module_node)
25
- case type(class_or_module_node)
15
+ case type_of(class_or_module_node)
26
16
  when CLASS, MODULE
27
17
  # (class (const nil :Foo) (const nil :Bar) (nil))
28
18
  # "class Foo < Bar; end"
@@ -36,10 +26,10 @@ module Packwerk
36
26
  end
37
27
 
38
28
  def constant_name(constant_node)
39
- case type(constant_node)
29
+ case type_of(constant_node)
40
30
  when CONSTANT_ROOT_NAMESPACE
41
31
  ""
42
- when CONSTANT, CONSTANT_ASSIGNMENT
32
+ when CONSTANT, CONSTANT_ASSIGNMENT, SELF
43
33
  # (const nil :Foo)
44
34
  # "Foo"
45
35
  # (const (cbase) :Foo)
@@ -52,6 +42,8 @@ module Packwerk
52
42
  # "::Foo = 1"
53
43
  # (casgn (lvar :a) :Foo (int 1))
54
44
  # "a::Foo = 1"
45
+ # (casgn (self) :Foo (int 1))
46
+ # "self::Foo = 1"
55
47
  namespace, name = constant_node.children
56
48
  if namespace
57
49
  [constant_name(namespace), name].join("::")
@@ -74,19 +66,19 @@ module Packwerk
74
66
  end
75
67
 
76
68
  def enclosing_namespace_path(starting_node, ancestors:)
77
- ancestors.select { |n| [CLASS, MODULE].include?(type(n)) }
69
+ ancestors.select { |n| [CLASS, MODULE].include?(type_of(n)) }
78
70
  .each_with_object([]) do |node, namespace|
79
71
  # when evaluating `class Child < Parent`, the const node for `Parent` is a child of the class
80
72
  # node, so it'll be an ancestor, but `Parent` is not evaluated in the namespace of `Child`, so
81
73
  # we need to skip it here
82
- next if type(node) == CLASS && parent_class(node) == starting_node
74
+ next if type_of(node) == CLASS && parent_class(node) == starting_node
83
75
 
84
76
  namespace.prepend(class_or_module_name(node))
85
77
  end
86
78
  end
87
79
 
88
80
  def literal_value(string_or_symbol_node)
89
- case type(string_or_symbol_node)
81
+ case type_of(string_or_symbol_node)
90
82
  when STRING, SYMBOL
91
83
  # (str "foo")
92
84
  # "'foo'"
@@ -103,8 +95,36 @@ module Packwerk
103
95
  Location.new(location.line, location.column)
104
96
  end
105
97
 
98
+ def constant?(node)
99
+ type_of(node) == CONSTANT
100
+ end
101
+
102
+ def constant_assignment?(node)
103
+ type_of(node) == CONSTANT_ASSIGNMENT
104
+ end
105
+
106
+ def class?(node)
107
+ type_of(node) == CLASS
108
+ end
109
+
110
+ def method_call?(node)
111
+ type_of(node) == METHOD_CALL
112
+ end
113
+
114
+ def hash?(node)
115
+ type_of(node) == HASH
116
+ end
117
+
118
+ def string?(node)
119
+ type_of(node) == STRING
120
+ end
121
+
122
+ def symbol?(node)
123
+ type_of(node) == SYMBOL
124
+ end
125
+
106
126
  def method_arguments(method_call_node)
107
- raise TypeError unless type(method_call_node) == METHOD_CALL
127
+ raise TypeError unless method_call?(method_call_node)
108
128
 
109
129
  # (send (lvar :foo) :bar (int 1))
110
130
  # "foo.bar(1)"
@@ -112,7 +132,7 @@ module Packwerk
112
132
  end
113
133
 
114
134
  def method_name(method_call_node)
115
- raise TypeError unless type(method_call_node) == METHOD_CALL
135
+ raise TypeError unless method_call?(method_call_node)
116
136
 
117
137
  # (send (lvar :foo) :bar (int 1))
118
138
  # "foo.bar(1)"
@@ -120,7 +140,7 @@ module Packwerk
120
140
  end
121
141
 
122
142
  def module_name_from_definition(node)
123
- case type(node)
143
+ case type_of(node)
124
144
  when CLASS, MODULE
125
145
  # "class My::Class; end"
126
146
  # "module My::Module; end"
@@ -130,7 +150,7 @@ module Packwerk
130
150
  # "My::Module = ..."
131
151
  rvalue = node.children.last
132
152
 
133
- case type(rvalue)
153
+ case type_of(rvalue)
134
154
  when METHOD_CALL
135
155
  # "Class.new"
136
156
  # "Module.new"
@@ -153,16 +173,17 @@ module Packwerk
153
173
  end
154
174
 
155
175
  def parent_class(class_node)
156
- raise TypeError unless type(class_node) == CLASS
176
+ raise TypeError unless type_of(class_node) == CLASS
157
177
 
158
178
  # (class (const nil :Foo) (const nil :Bar) (nil))
159
179
  # "class Foo < Bar; end"
160
180
  class_node.children[1]
161
181
  end
162
182
 
183
+ sig { params(ancestors: T::Array[AST::Node]).returns(String) }
163
184
  def parent_module_name(ancestors:)
164
185
  definitions = ancestors
165
- .select { |n| [CLASS, MODULE, CONSTANT_ASSIGNMENT, BLOCK].include?(type(n)) }
186
+ .select { |n| [CLASS, MODULE, CONSTANT_ASSIGNMENT, BLOCK].include?(type_of(n)) }
166
187
 
167
188
  names = definitions.map do |definition|
168
189
  name_part_from_definition(definition)
@@ -171,20 +192,38 @@ module Packwerk
171
192
  names.empty? ? "Object" : names.reverse.join("::")
172
193
  end
173
194
 
174
- def type(node)
175
- node.type
176
- end
177
-
178
195
  def value_from_hash(hash_node, key)
179
- raise TypeError unless type(hash_node) == HASH
196
+ raise TypeError unless hash?(hash_node)
180
197
  pair = hash_pairs(hash_node).detect { |pair_node| literal_value(hash_pair_key(pair_node)) == key }
181
198
  hash_pair_value(pair) if pair
182
199
  end
183
200
 
184
201
  private
185
202
 
203
+ BLOCK = :block
204
+ CLASS = :class
205
+ CONSTANT = :const
206
+ CONSTANT_ASSIGNMENT = :casgn
207
+ CONSTANT_ROOT_NAMESPACE = :cbase
208
+ HASH = :hash
209
+ HASH_PAIR = :pair
210
+ METHOD_CALL = :send
211
+ MODULE = :module
212
+ SELF = :self
213
+ STRING = :str
214
+ SYMBOL = :sym
215
+
216
+ private_constant(
217
+ :BLOCK, :CLASS, :CONSTANT, :CONSTANT_ASSIGNMENT, :CONSTANT_ROOT_NAMESPACE, :HASH, :HASH_PAIR, :METHOD_CALL,
218
+ :MODULE, :SELF, :STRING, :SYMBOL,
219
+ )
220
+
221
+ def type_of(node)
222
+ node.type
223
+ end
224
+
186
225
  def hash_pair_key(hash_pair_node)
187
- raise TypeError unless type(hash_pair_node) == HASH_PAIR
226
+ raise TypeError unless type_of(hash_pair_node) == HASH_PAIR
188
227
 
189
228
  # (pair (int 1) (int 2))
190
229
  # "1 => 2"
@@ -194,7 +233,7 @@ module Packwerk
194
233
  end
195
234
 
196
235
  def hash_pair_value(hash_pair_node)
197
- raise TypeError unless type(hash_pair_node) == HASH_PAIR
236
+ raise TypeError unless type_of(hash_pair_node) == HASH_PAIR
198
237
 
199
238
  # (pair (int 1) (int 2))
200
239
  # "1 => 2"
@@ -204,15 +243,15 @@ module Packwerk
204
243
  end
205
244
 
206
245
  def hash_pairs(hash_node)
207
- raise TypeError unless type(hash_node) == HASH
246
+ raise TypeError unless hash?(hash_node)
208
247
 
209
248
  # (hash (pair (int 1) (int 2)) (pair (int 3) (int 4)))
210
249
  # "{1 => 2, 3 => 4}"
211
- hash_node.children.select { |n| type(n) == HASH_PAIR }
250
+ hash_node.children.select { |n| type_of(n) == HASH_PAIR }
212
251
  end
213
252
 
214
253
  def method_call_node(block_node)
215
- raise TypeError unless type(block_node) == BLOCK
254
+ raise TypeError unless type_of(block_node) == BLOCK
216
255
 
217
256
  # (block (send (lvar :foo) :bar) (args) (int 42))
218
257
  # "foo.bar do 42 end"
@@ -222,8 +261,8 @@ module Packwerk
222
261
  def module_creation?(node)
223
262
  # "Class.new"
224
263
  # "Module.new"
225
- type(node) == METHOD_CALL &&
226
- type(receiver(node)) == CONSTANT &&
264
+ method_call?(node) &&
265
+ constant?(receiver(node)) &&
227
266
  ["Class", "Module"].include?(constant_name(receiver(node))) &&
228
267
  method_name(node) == :new
229
268
  end
@@ -231,12 +270,12 @@ module Packwerk
231
270
  def name_from_block_definition(node)
232
271
  if method_name(method_call_node(node)) == :class_eval
233
272
  receiver = receiver(node)
234
- constant_name(receiver) if receiver && type(receiver) == CONSTANT
273
+ constant_name(receiver) if receiver && constant?(receiver)
235
274
  end
236
275
  end
237
276
 
238
277
  def name_part_from_definition(node)
239
- case type(node)
278
+ case type_of(node)
240
279
  when CLASS, MODULE, CONSTANT_ASSIGNMENT
241
280
  module_name_from_definition(node)
242
281
  when BLOCK
@@ -245,7 +284,7 @@ module Packwerk
245
284
  end
246
285
 
247
286
  def receiver(method_call_or_block_node)
248
- case type(method_call_or_block_node)
287
+ case type_of(method_call_or_block_node)
249
288
  when METHOD_CALL
250
289
  method_call_or_block_node.children[0]
251
290
  when BLOCK
@@ -3,9 +3,21 @@
3
3
 
4
4
  require "packwerk/node"
5
5
  require "packwerk/offense"
6
+ require "packwerk/checker"
7
+ require "packwerk/reference_lister"
6
8
 
7
9
  module Packwerk
8
10
  class NodeProcessor
11
+ extend T::Sig
12
+
13
+ sig do
14
+ params(
15
+ reference_extractor: ReferenceExtractor,
16
+ reference_lister: ReferenceLister,
17
+ filename: String,
18
+ checkers: T::Array[Checker]
19
+ ).void
20
+ end
9
21
  def initialize(reference_extractor:, reference_lister:, filename:, checkers:)
10
22
  @reference_extractor = reference_extractor
11
23
  @reference_lister = reference_lister
@@ -14,8 +26,7 @@ module Packwerk
14
26
  end
15
27
 
16
28
  def call(node, ancestors:)
17
- case Node.type(node)
18
- when Node::METHOD_CALL, Node::CONSTANT
29
+ if Node.method_call?(node) || Node.constant?(node)
19
30
  reference = @reference_extractor.reference_from_node(node, ancestors: ancestors, file_path: @filename)
20
31
  check_reference(reference, node) if reference
21
32
  end
@@ -30,7 +41,7 @@ module Packwerk
30
41
 
31
42
  Packwerk::Offense.new(
32
43
  location: Node.location(node),
33
- file: @filename,
44
+ file: reference.relative_path,
34
45
  message: <<~EOS
35
46
  #{message}
36
47
  Inference details: this is a reference to #{constant.name} which seems to be defined in #{constant.location}.
@@ -0,0 +1,39 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "packwerk/constant_name_inspector"
5
+ require "packwerk/checker"
6
+
7
+ module Packwerk
8
+ class NodeProcessorFactory < T::Struct
9
+ extend T::Sig
10
+
11
+ const :root_path, String
12
+ const :reference_lister, ReferenceLister
13
+ const :context_provider, Packwerk::ConstantDiscovery
14
+ const :constant_name_inspectors, T::Array[ConstantNameInspector]
15
+ const :checkers, T::Array[Checker]
16
+
17
+ sig { params(filename: String, node: AST::Node).returns(NodeProcessor) }
18
+ def for(filename:, node:)
19
+ ::Packwerk::NodeProcessor.new(
20
+ reference_extractor: reference_extractor(node: node),
21
+ reference_lister: reference_lister,
22
+ filename: filename,
23
+ checkers: checkers,
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ sig { params(node: AST::Node).returns(::Packwerk::ReferenceExtractor) }
30
+ def reference_extractor(node:)
31
+ ::Packwerk::ReferenceExtractor.new(
32
+ context_provider: context_provider,
33
+ constant_name_inspectors: constant_name_inspectors,
34
+ root_node: node,
35
+ root_path: root_path,
36
+ )
37
+ end
38
+ end
39
+ end
@@ -4,7 +4,8 @@
4
4
  require "parser/source/map"
5
5
  require "sorbet-runtime"
6
6
 
7
- require "packwerk/output_styles"
7
+ require "packwerk/output_style"
8
+ require "packwerk/output_styles/plain"
8
9
 
9
10
  module Packwerk
10
11
  class Offense
@@ -23,11 +24,8 @@ module Packwerk
23
24
  @message = message
24
25
  end
25
26
 
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)
27
+ sig { params(style: OutputStyle).returns(String) }
28
+ def to_s(style = OutputStyles::Plain.new)
31
29
  if location
32
30
  <<~EOS
33
31
  #{style.filename}#{file}#{style.reset}:#{location.line}:#{location.column}