packwerk 1.0.0 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
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}