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.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
  3. data/.github/probots.yml +2 -0
  4. data/.github/pull_request_template.md +27 -0
  5. data/.github/workflows/ci.yml +50 -0
  6. data/.gitignore +12 -0
  7. data/.rubocop.yml +46 -0
  8. data/.ruby-version +1 -0
  9. data/CODEOWNERS +1 -0
  10. data/CODE_OF_CONDUCT.md +76 -0
  11. data/CONTRIBUTING.md +17 -0
  12. data/Gemfile +22 -0
  13. data/Gemfile.lock +236 -0
  14. data/LICENSE.md +7 -0
  15. data/README.md +73 -0
  16. data/Rakefile +13 -0
  17. data/TROUBLESHOOT.md +67 -0
  18. data/USAGE.md +250 -0
  19. data/bin/console +15 -0
  20. data/bin/setup +8 -0
  21. data/dev.yml +32 -0
  22. data/docs/cohesion.png +0 -0
  23. data/exe/packwerk +6 -0
  24. data/lib/packwerk.rb +44 -0
  25. data/lib/packwerk/application_validator.rb +343 -0
  26. data/lib/packwerk/association_inspector.rb +44 -0
  27. data/lib/packwerk/checking_deprecated_references.rb +40 -0
  28. data/lib/packwerk/cli.rb +238 -0
  29. data/lib/packwerk/configuration.rb +82 -0
  30. data/lib/packwerk/const_node_inspector.rb +44 -0
  31. data/lib/packwerk/constant_discovery.rb +60 -0
  32. data/lib/packwerk/constant_name_inspector.rb +22 -0
  33. data/lib/packwerk/dependency_checker.rb +28 -0
  34. data/lib/packwerk/deprecated_references.rb +92 -0
  35. data/lib/packwerk/file_processor.rb +43 -0
  36. data/lib/packwerk/files_for_processing.rb +67 -0
  37. data/lib/packwerk/formatters/progress_formatter.rb +46 -0
  38. data/lib/packwerk/generators/application_validation.rb +62 -0
  39. data/lib/packwerk/generators/configuration_file.rb +69 -0
  40. data/lib/packwerk/generators/inflections_file.rb +43 -0
  41. data/lib/packwerk/generators/root_package.rb +37 -0
  42. data/lib/packwerk/generators/templates/inflections.yml +6 -0
  43. data/lib/packwerk/generators/templates/package.yml +17 -0
  44. data/lib/packwerk/generators/templates/packwerk +23 -0
  45. data/lib/packwerk/generators/templates/packwerk.yml.erb +23 -0
  46. data/lib/packwerk/generators/templates/packwerk_validator_test.rb +11 -0
  47. data/lib/packwerk/graph.rb +74 -0
  48. data/lib/packwerk/inflections/custom.rb +33 -0
  49. data/lib/packwerk/inflections/default.rb +73 -0
  50. data/lib/packwerk/inflector.rb +41 -0
  51. data/lib/packwerk/node.rb +259 -0
  52. data/lib/packwerk/node_processor.rb +49 -0
  53. data/lib/packwerk/node_visitor.rb +22 -0
  54. data/lib/packwerk/offense.rb +44 -0
  55. data/lib/packwerk/output_styles.rb +41 -0
  56. data/lib/packwerk/package.rb +56 -0
  57. data/lib/packwerk/package_set.rb +59 -0
  58. data/lib/packwerk/parsed_constant_definitions.rb +62 -0
  59. data/lib/packwerk/parsers.rb +23 -0
  60. data/lib/packwerk/parsers/erb.rb +66 -0
  61. data/lib/packwerk/parsers/factory.rb +34 -0
  62. data/lib/packwerk/parsers/ruby.rb +42 -0
  63. data/lib/packwerk/privacy_checker.rb +45 -0
  64. data/lib/packwerk/reference.rb +6 -0
  65. data/lib/packwerk/reference_extractor.rb +81 -0
  66. data/lib/packwerk/reference_lister.rb +23 -0
  67. data/lib/packwerk/run_context.rb +103 -0
  68. data/lib/packwerk/sanity_checker.rb +10 -0
  69. data/lib/packwerk/spring_command.rb +28 -0
  70. data/lib/packwerk/updating_deprecated_references.rb +51 -0
  71. data/lib/packwerk/version.rb +6 -0
  72. data/lib/packwerk/violation_type.rb +13 -0
  73. data/library.yml +6 -0
  74. data/packwerk.gemspec +58 -0
  75. data/service.yml +6 -0
  76. data/shipit.rubygems.yml +1 -0
  77. data/sorbet/config +2 -0
  78. data/sorbet/rbi/gems/actioncable@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +840 -0
  79. data/sorbet/rbi/gems/actionmailbox@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +571 -0
  80. data/sorbet/rbi/gems/actionmailer@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +568 -0
  81. data/sorbet/rbi/gems/actionpack@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +5216 -0
  82. data/sorbet/rbi/gems/actiontext@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +663 -0
  83. data/sorbet/rbi/gems/actionview@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +2504 -0
  84. data/sorbet/rbi/gems/activejob@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +635 -0
  85. data/sorbet/rbi/gems/activemodel@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +1201 -0
  86. data/sorbet/rbi/gems/activerecord@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +8011 -0
  87. data/sorbet/rbi/gems/activestorage@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +904 -0
  88. data/sorbet/rbi/gems/activesupport@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +3888 -0
  89. data/sorbet/rbi/gems/ast@2.4.1.rbi +54 -0
  90. data/sorbet/rbi/gems/better_html@1.0.15.rbi +317 -0
  91. data/sorbet/rbi/gems/builder@3.2.4.rbi +8 -0
  92. data/sorbet/rbi/gems/byebug@11.1.3.rbi +8 -0
  93. data/sorbet/rbi/gems/coderay@1.1.3.rbi +8 -0
  94. data/sorbet/rbi/gems/colorize@0.8.1.rbi +40 -0
  95. data/sorbet/rbi/gems/commander@4.5.2.rbi +8 -0
  96. data/sorbet/rbi/gems/concurrent-ruby@1.1.6.rbi +1966 -0
  97. data/sorbet/rbi/gems/constant_resolver@0.1.5.rbi +26 -0
  98. data/sorbet/rbi/gems/crass@1.0.6.rbi +138 -0
  99. data/sorbet/rbi/gems/erubi@1.9.0.rbi +39 -0
  100. data/sorbet/rbi/gems/globalid@0.4.2.rbi +178 -0
  101. data/sorbet/rbi/gems/highline@2.0.3.rbi +8 -0
  102. data/sorbet/rbi/gems/html_tokenizer@0.0.7.rbi +46 -0
  103. data/sorbet/rbi/gems/i18n@1.8.2.rbi +633 -0
  104. data/sorbet/rbi/gems/jaro_winkler@1.5.4.rbi +8 -0
  105. data/sorbet/rbi/gems/loofah@2.5.0.rbi +272 -0
  106. data/sorbet/rbi/gems/m@1.5.1.rbi +108 -0
  107. data/sorbet/rbi/gems/mail@2.7.1.rbi +2490 -0
  108. data/sorbet/rbi/gems/marcel@0.3.3.rbi +30 -0
  109. data/sorbet/rbi/gems/method_source@1.0.0.rbi +76 -0
  110. data/sorbet/rbi/gems/mimemagic@0.3.5.rbi +47 -0
  111. data/sorbet/rbi/gems/mini_mime@1.0.2.rbi +71 -0
  112. data/sorbet/rbi/gems/mini_portile2@2.4.0.rbi +8 -0
  113. data/sorbet/rbi/gems/minitest@5.14.0.rbi +542 -0
  114. data/sorbet/rbi/gems/mocha@1.11.2.rbi +964 -0
  115. data/sorbet/rbi/gems/nio4r@2.5.2.rbi +89 -0
  116. data/sorbet/rbi/gems/nokogiri@1.10.9.rbi +1608 -0
  117. data/sorbet/rbi/gems/parallel@1.19.1.rbi +8 -0
  118. data/sorbet/rbi/gems/parlour@4.0.1.rbi +561 -0
  119. data/sorbet/rbi/gems/parser@2.7.1.4.rbi +1632 -0
  120. data/sorbet/rbi/gems/pry@0.13.1.rbi +8 -0
  121. data/sorbet/rbi/gems/rack-test@1.1.0.rbi +335 -0
  122. data/sorbet/rbi/gems/rack@2.2.2.rbi +1730 -0
  123. data/sorbet/rbi/gems/rails-dom-testing@2.0.3.rbi +123 -0
  124. data/sorbet/rbi/gems/rails-html-sanitizer@1.3.0.rbi +213 -0
  125. data/sorbet/rbi/gems/rails@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +8 -0
  126. data/sorbet/rbi/gems/railties@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +869 -0
  127. data/sorbet/rbi/gems/rainbow@3.0.0.rbi +155 -0
  128. data/sorbet/rbi/gems/rake@13.0.1.rbi +841 -0
  129. data/sorbet/rbi/gems/rexml@3.2.4.rbi +8 -0
  130. data/sorbet/rbi/gems/rubocop-performance@1.5.2.rbi +8 -0
  131. data/sorbet/rbi/gems/rubocop-shopify@1.0.2.rbi +8 -0
  132. data/sorbet/rbi/gems/rubocop-sorbet@0.3.7.rbi +8 -0
  133. data/sorbet/rbi/gems/rubocop@0.82.0.rbi +8 -0
  134. data/sorbet/rbi/gems/ruby-progressbar@1.10.1.rbi +8 -0
  135. data/sorbet/rbi/gems/smart_properties@1.15.0.rbi +168 -0
  136. data/sorbet/rbi/gems/spoom@1.0.4.rbi +418 -0
  137. data/sorbet/rbi/gems/spring@2.1.0.rbi +160 -0
  138. data/sorbet/rbi/gems/sprockets-rails@3.2.1.rbi +431 -0
  139. data/sorbet/rbi/gems/sprockets@4.0.0.rbi +1132 -0
  140. data/sorbet/rbi/gems/tapioca@0.4.5.rbi +518 -0
  141. data/sorbet/rbi/gems/thor@1.0.1.rbi +892 -0
  142. data/sorbet/rbi/gems/tzinfo@2.0.2.rbi +547 -0
  143. data/sorbet/rbi/gems/unicode-display_width@1.7.0.rbi +8 -0
  144. data/sorbet/rbi/gems/websocket-driver@0.7.1.rbi +438 -0
  145. data/sorbet/rbi/gems/websocket-extensions@0.1.4.rbi +71 -0
  146. data/sorbet/rbi/gems/zeitwerk@2.3.0.rbi +8 -0
  147. data/sorbet/tapioca/require.rb +25 -0
  148. data/static/packwerk-check-demo.png +0 -0
  149. data/static/packwerk_check.gif +0 -0
  150. data/static/packwerk_check_violation.gif +0 -0
  151. data/static/packwerk_update.gif +0 -0
  152. data/static/packwerk_validate.gif +0 -0
  153. metadata +341 -0
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Generators
6
+ class RootPackage
7
+ extend T::Sig
8
+
9
+ class << self
10
+ def generate(root:, out:)
11
+ new(root: root, out: out).generate
12
+ end
13
+ end
14
+
15
+ def initialize(root:, out: $stdout)
16
+ @root = root
17
+ @out = out
18
+ end
19
+
20
+ sig { returns(T::Boolean) }
21
+ def generate
22
+ if Dir.glob("#{@root}/package.yml").any?
23
+ @out.puts("⚠️ Root package already exists.")
24
+ return true
25
+ end
26
+
27
+ @out.puts("📦 Generating `package.yml` file for root package...")
28
+
29
+ source_file_path = File.join(__dir__, "/templates/package.yml")
30
+ FileUtils.cp(source_file_path, @root)
31
+
32
+ @out.puts("✅ `package.yml` for the root package generated in #{@root}")
33
+ true
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ # List your inflections in this file instead of `inflections.rb`
2
+ # See steps to set up custom inflections:
3
+ # https://github.com/Shopify/packwerk/blob/main/USAGE.md#Inflections
4
+
5
+ # acronym:
6
+ # - "GraphQL"
@@ -0,0 +1,17 @@
1
+ # This file represents the root package of the application
2
+ # Please validate the configuration using `bin/packwerk validate` (for Rails applications) or running the auto generated
3
+ # test case (for non-Rails projects). You can then use `packwerk check` to check your code.
4
+
5
+ # Turn on dependency checks for this package
6
+ enforce_dependencies: true
7
+
8
+ # Turn on privacy checks for this package
9
+ # enforcing privacy is often not useful for the root package, because it would require defining a public interface
10
+ # for something that should only be a thin wrapper in the first place.
11
+ # We recommend enabling this for any new packages you create to aid with encapsulation.
12
+ enforce_privacy: false
13
+
14
+ # A list of this package's dependencies
15
+ # Note that packages in this list require their own `package.yml` file
16
+ # dependencies:
17
+ # - "packages/billing"
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # This file was auto-generated by Packwerk through `packwerk init`
5
+
6
+ # Needs to be run in test environment in order to have test helper paths available in the autoload paths
7
+ ENV["RAILS_ENV"] = "test"
8
+
9
+ # Command line arguments needs to be duplicated because spring modifies it
10
+ packwerk_argv = ARGV.dup
11
+
12
+ begin
13
+ load(File.expand_path("../spring", __FILE__))
14
+ rescue LoadError => e
15
+ raise unless e.message.include?("spring")
16
+ end
17
+
18
+ require File.expand_path("../../config/environment", __FILE__)
19
+
20
+ require "packwerk"
21
+
22
+ cli = Packwerk::Cli.new
23
+ cli.run(packwerk_argv)
@@ -0,0 +1,23 @@
1
+ # See: Setting up the configuration file
2
+ # https://github.com/Shopify/packwerk/blob/main/USAGE.md#setting-up-the-configuration-file
3
+
4
+ # List of patterns for folder paths to include
5
+ # include:
6
+ # - "**/*.{rb,rake,erb}"
7
+
8
+ # List of patterns for folder paths to exclude
9
+ # exclude:
10
+ # - "{bin,node_modules,script,tmp}/**/*"
11
+
12
+ # Patterns to find package configuration files
13
+ # package_paths: "**/"
14
+
15
+ # List of application load paths
16
+ <%= @load_paths_comment -%>
17
+ <%= @load_paths_formatted %>
18
+ # List of custom associations, if any
19
+ # custom_associations:
20
+ # - "cache_belongs_to"
21
+
22
+ # Location of inflections file
23
+ # inflections_file: "config/inflections.yml"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "packwerk"
5
+
6
+ # This test is necessary to make sure that the package system is working correctly
7
+ class PackwerkValidatorTest < Minitest::Test
8
+ def test_the_application_is_correctly_set_up_for_the_package_system
9
+ assert(Packwerk::Cli.new.execute_command(["validate"]))
10
+ end
11
+ end
@@ -0,0 +1,74 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ class Graph
6
+ def initialize(*edges)
7
+ @edges = edges.uniq
8
+ @cycles = Set.new
9
+ process
10
+ end
11
+
12
+ def cycles
13
+ @cycles.dup
14
+ end
15
+
16
+ def acyclic?
17
+ @cycles.empty?
18
+ end
19
+
20
+ private
21
+
22
+ def nodes
23
+ @edges.flatten.uniq
24
+ end
25
+
26
+ def process
27
+ # See https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
28
+ @processed ||= begin
29
+ nodes.each { |node| visit(node) }
30
+ true
31
+ end
32
+ end
33
+
34
+ def visit(node, visited_nodes: Set.new, path: [])
35
+ # Already visited, short circuit to avoid unnecessary processing
36
+ return if visited_nodes.include?(node)
37
+
38
+ # We've returned to a node that we've already visited, so we've found a cycle!
39
+ if path.include?(node)
40
+ # Filter out the part of the path that isn't a cycle. For example, with the following path:
41
+ #
42
+ # a -> b -> c -> d -> b
43
+ #
44
+ # "a" isn't part of the cycle. The cycle should only appear once in the path, so we reject
45
+ # everything from the beginning to the first instance of the current node.
46
+ add_cycle(path.drop_while { |n| n != node })
47
+ return
48
+ end
49
+
50
+ path << node
51
+ neighbours(node).each do |neighbour|
52
+ visit(neighbour, visited_nodes: visited_nodes, path: path)
53
+ end
54
+ path.pop
55
+ ensure
56
+ visited_nodes << node
57
+ end
58
+
59
+ def neighbours(node)
60
+ @edges
61
+ .lazy
62
+ .select { |src, _dst| src == node }
63
+ .map { |_src, dst| dst }
64
+ end
65
+
66
+ def add_cycle(cycle)
67
+ # Ensure that the lexicographically smallest item is the first one labeled in a cycle
68
+ min_node = cycle.min
69
+ cycle.rotate! until cycle.first == min_node
70
+
71
+ @cycles << cycle
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,33 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "yaml"
5
+
6
+ module Packwerk
7
+ module Inflections
8
+ class Custom
9
+ SUPPORTED_INFLECTION_METHODS = %w(acronym human irregular plural singular uncountable)
10
+
11
+ attr_accessor :inflections
12
+
13
+ def initialize(custom_inflection_file = nil)
14
+ if custom_inflection_file && File.exist?(custom_inflection_file)
15
+ @inflections = YAML.load_file(custom_inflection_file) || {}
16
+
17
+ invalid_inflections = @inflections.keys - SUPPORTED_INFLECTION_METHODS
18
+ raise ArgumentError, "Unsupported inflection types: #{invalid_inflections}" if invalid_inflections.any?
19
+ else
20
+ @inflections = []
21
+ end
22
+ end
23
+
24
+ def apply_to(inflections_object)
25
+ @inflections.each do |inflection_type, inflections|
26
+ inflections.each do |inflection|
27
+ inflections_object.public_send(inflection_type, *Array(inflection))
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,73 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Packwerk
5
+ module Inflections
6
+ module Default
7
+ class << self
8
+ def apply_to(inflections_object)
9
+ # copied from active_support/inflections
10
+ # https://github.com/rails/rails/blob/d2ae2c3103e93783971d5356d0b3fd1b4070d6cf/activesupport/lib/active_support/inflections.rb#L12
11
+ inflections_object.plural(/$/, "s")
12
+ inflections_object.plural(/s$/i, "s")
13
+ inflections_object.plural(/^(ax|test)is$/i, '\1es')
14
+ inflections_object.plural(/(octop|vir)us$/i, '\1i')
15
+ inflections_object.plural(/(octop|vir)i$/i, '\1i')
16
+ inflections_object.plural(/(alias|status)$/i, '\1es')
17
+ inflections_object.plural(/(bu)s$/i, '\1ses')
18
+ inflections_object.plural(/(buffal|tomat)o$/i, '\1oes')
19
+ inflections_object.plural(/([ti])um$/i, '\1a')
20
+ inflections_object.plural(/([ti])a$/i, '\1a')
21
+ inflections_object.plural(/sis$/i, "ses")
22
+ inflections_object.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
23
+ inflections_object.plural(/(hive)$/i, '\1s')
24
+ inflections_object.plural(/([^aeiouy]|qu)y$/i, '\1ies')
25
+ inflections_object.plural(/(x|ch|ss|sh)$/i, '\1es')
26
+ inflections_object.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
27
+ inflections_object.plural(/^(m|l)ouse$/i, '\1ice')
28
+ inflections_object.plural(/^(m|l)ice$/i, '\1ice')
29
+ inflections_object.plural(/^(ox)$/i, '\1en')
30
+ inflections_object.plural(/^(oxen)$/i, '\1')
31
+ inflections_object.plural(/(quiz)$/i, '\1zes')
32
+
33
+ inflections_object.singular(/s$/i, "")
34
+ inflections_object.singular(/(ss)$/i, '\1')
35
+ inflections_object.singular(/(n)ews$/i, '\1ews')
36
+ inflections_object.singular(/([ti])a$/i, '\1um')
37
+ inflections_object.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1sis')
38
+ inflections_object.singular(/(^analy)(sis|ses)$/i, '\1sis')
39
+ inflections_object.singular(/([^f])ves$/i, '\1fe')
40
+ inflections_object.singular(/(hive)s$/i, '\1')
41
+ inflections_object.singular(/(tive)s$/i, '\1')
42
+ inflections_object.singular(/([lr])ves$/i, '\1f')
43
+ inflections_object.singular(/([^aeiouy]|qu)ies$/i, '\1y')
44
+ inflections_object.singular(/(s)eries$/i, '\1eries')
45
+ inflections_object.singular(/(m)ovies$/i, '\1ovie')
46
+ inflections_object.singular(/(x|ch|ss|sh)es$/i, '\1')
47
+ inflections_object.singular(/^(m|l)ice$/i, '\1ouse')
48
+ inflections_object.singular(/(bus)(es)?$/i, '\1')
49
+ inflections_object.singular(/(o)es$/i, '\1')
50
+ inflections_object.singular(/(shoe)s$/i, '\1')
51
+ inflections_object.singular(/(cris|test)(is|es)$/i, '\1is')
52
+ inflections_object.singular(/^(a)x[ie]s$/i, '\1xis')
53
+ inflections_object.singular(/(octop|vir)(us|i)$/i, '\1us')
54
+ inflections_object.singular(/(alias|status)(es)?$/i, '\1')
55
+ inflections_object.singular(/^(ox)en/i, '\1')
56
+ inflections_object.singular(/(vert|ind)ices$/i, '\1ex')
57
+ inflections_object.singular(/(matr)ices$/i, '\1ix')
58
+ inflections_object.singular(/(quiz)zes$/i, '\1')
59
+ inflections_object.singular(/(database)s$/i, '\1')
60
+
61
+ inflections_object.irregular("person", "people")
62
+ inflections_object.irregular("man", "men")
63
+ inflections_object.irregular("child", "children")
64
+ inflections_object.irregular("sex", "sexes")
65
+ inflections_object.irregular("move", "moves")
66
+ inflections_object.irregular("zombie", "zombies")
67
+
68
+ inflections_object.uncountable(%w(equipment information rice money species series fish sheep jeans police))
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,41 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "active_support/inflector"
5
+ require "packwerk/inflections/default"
6
+ require "packwerk/inflections/custom"
7
+
8
+ module Packwerk
9
+ class Inflector
10
+ class << self
11
+ def default
12
+ @default ||= new
13
+ end
14
+ end
15
+
16
+ # For #camelize, #classify, #pluralize, #singularize
17
+ include ::ActiveSupport::Inflector
18
+
19
+ def initialize(custom_inflection_file: nil)
20
+ @inflections = ::ActiveSupport::Inflector::Inflections.new
21
+
22
+ Inflections::Default.apply_to(@inflections)
23
+
24
+ Inflections::Custom.new(custom_inflection_file).apply_to(@inflections)
25
+ end
26
+
27
+ def pluralize(word, count = nil)
28
+ if count == 1
29
+ singularize(word)
30
+ else
31
+ super(word)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def inflections(_ = nil)
38
+ @inflections
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,259 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "parser/ast/node"
5
+
6
+ module Packwerk
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
+ class TypeError < ArgumentError; end
21
+ Location = Struct.new(:line, :column)
22
+
23
+ class << self
24
+ def class_or_module_name(class_or_module_node)
25
+ case type(class_or_module_node)
26
+ when CLASS, MODULE
27
+ # (class (const nil :Foo) (const nil :Bar) (nil))
28
+ # "class Foo < Bar; end"
29
+ # (module (const nil :Foo) (nil))
30
+ # "module Foo; end"
31
+ identifier = class_or_module_node.children[0]
32
+ constant_name(identifier)
33
+ else
34
+ raise TypeError
35
+ end
36
+ end
37
+
38
+ def constant_name(constant_node)
39
+ case type(constant_node)
40
+ when CONSTANT_ROOT_NAMESPACE
41
+ ""
42
+ when CONSTANT, CONSTANT_ASSIGNMENT
43
+ # (const nil :Foo)
44
+ # "Foo"
45
+ # (const (cbase) :Foo)
46
+ # "::Foo"
47
+ # (const (lvar :a) :Foo)
48
+ # "a::Foo"
49
+ # (casgn nil :Foo (int 1))
50
+ # "Foo = 1"
51
+ # (casgn (cbase) :Foo (int 1))
52
+ # "::Foo = 1"
53
+ # (casgn (lvar :a) :Foo (int 1))
54
+ # "a::Foo = 1"
55
+ namespace, name = constant_node.children
56
+ if namespace
57
+ [constant_name(namespace), name].join("::")
58
+ else
59
+ name.to_s
60
+ end
61
+ else
62
+ raise TypeError
63
+ end
64
+ end
65
+
66
+ def each_child(node)
67
+ if block_given?
68
+ node.children.each do |child|
69
+ yield child if child.is_a?(Parser::AST::Node)
70
+ end
71
+ else
72
+ enum_for(:each_child, node)
73
+ end
74
+ end
75
+
76
+ def enclosing_namespace_path(starting_node, ancestors:)
77
+ ancestors.select { |n| [CLASS, MODULE].include?(type(n)) }
78
+ .each_with_object([]) do |node, namespace|
79
+ # when evaluating `class Child < Parent`, the const node for `Parent` is a child of the class
80
+ # node, so it'll be an ancestor, but `Parent` is not evaluated in the namespace of `Child`, so
81
+ # we need to skip it here
82
+ next if type(node) == CLASS && parent_class(node) == starting_node
83
+
84
+ namespace.prepend(class_or_module_name(node))
85
+ end
86
+ end
87
+
88
+ def literal_value(string_or_symbol_node)
89
+ case type(string_or_symbol_node)
90
+ when STRING, SYMBOL
91
+ # (str "foo")
92
+ # "'foo'"
93
+ # (sym :foo)
94
+ # ":foo"
95
+ string_or_symbol_node.children[0]
96
+ else
97
+ raise TypeError
98
+ end
99
+ end
100
+
101
+ def location(node)
102
+ location = node.location
103
+ Location.new(location.line, location.column)
104
+ end
105
+
106
+ def method_arguments(method_call_node)
107
+ raise TypeError unless type(method_call_node) == METHOD_CALL
108
+
109
+ # (send (lvar :foo) :bar (int 1))
110
+ # "foo.bar(1)"
111
+ method_call_node.children.slice(2..-1)
112
+ end
113
+
114
+ def method_name(method_call_node)
115
+ raise TypeError unless type(method_call_node) == METHOD_CALL
116
+
117
+ # (send (lvar :foo) :bar (int 1))
118
+ # "foo.bar(1)"
119
+ method_call_node.children[1]
120
+ end
121
+
122
+ def module_name_from_definition(node)
123
+ case type(node)
124
+ when CLASS, MODULE
125
+ # "class My::Class; end"
126
+ # "module My::Module; end"
127
+ class_or_module_name(node)
128
+ when CONSTANT_ASSIGNMENT
129
+ # "My::Class = ..."
130
+ # "My::Module = ..."
131
+ rvalue = node.children.last
132
+
133
+ case type(rvalue)
134
+ when METHOD_CALL
135
+ # "Class.new"
136
+ # "Module.new"
137
+ constant_name(node) if module_creation?(rvalue)
138
+ when BLOCK
139
+ # "Class.new do end"
140
+ # "Module.new do end"
141
+ constant_name(node) if module_creation?(method_call_node(rvalue))
142
+ end
143
+ end
144
+ end
145
+
146
+ def name_location(node)
147
+ location = node.location
148
+
149
+ if location.respond_to?(:name)
150
+ name = location.name
151
+ Location.new(name.line, name.column)
152
+ end
153
+ end
154
+
155
+ def parent_class(class_node)
156
+ raise TypeError unless type(class_node) == CLASS
157
+
158
+ # (class (const nil :Foo) (const nil :Bar) (nil))
159
+ # "class Foo < Bar; end"
160
+ class_node.children[1]
161
+ end
162
+
163
+ def parent_module_name(ancestors:)
164
+ definitions = ancestors
165
+ .select { |n| [CLASS, MODULE, CONSTANT_ASSIGNMENT, BLOCK].include?(type(n)) }
166
+
167
+ names = definitions.map do |definition|
168
+ name_part_from_definition(definition)
169
+ end.compact
170
+
171
+ names.empty? ? "Object" : names.reverse.join("::")
172
+ end
173
+
174
+ def type(node)
175
+ node.type
176
+ end
177
+
178
+ def value_from_hash(hash_node, key)
179
+ raise TypeError unless type(hash_node) == HASH
180
+ pair = hash_pairs(hash_node).detect { |pair_node| literal_value(hash_pair_key(pair_node)) == key }
181
+ hash_pair_value(pair) if pair
182
+ end
183
+
184
+ private
185
+
186
+ def hash_pair_key(hash_pair_node)
187
+ raise TypeError unless type(hash_pair_node) == HASH_PAIR
188
+
189
+ # (pair (int 1) (int 2))
190
+ # "1 => 2"
191
+ # (pair (sym :answer) (int 42))
192
+ # "answer: 42"
193
+ hash_pair_node.children[0]
194
+ end
195
+
196
+ def hash_pair_value(hash_pair_node)
197
+ raise TypeError unless type(hash_pair_node) == HASH_PAIR
198
+
199
+ # (pair (int 1) (int 2))
200
+ # "1 => 2"
201
+ # (pair (sym :answer) (int 42))
202
+ # "answer: 42"
203
+ hash_pair_node.children[1]
204
+ end
205
+
206
+ def hash_pairs(hash_node)
207
+ raise TypeError unless type(hash_node) == HASH
208
+
209
+ # (hash (pair (int 1) (int 2)) (pair (int 3) (int 4)))
210
+ # "{1 => 2, 3 => 4}"
211
+ hash_node.children.select { |n| type(n) == HASH_PAIR }
212
+ end
213
+
214
+ def method_call_node(block_node)
215
+ raise TypeError unless type(block_node) == BLOCK
216
+
217
+ # (block (send (lvar :foo) :bar) (args) (int 42))
218
+ # "foo.bar do 42 end"
219
+ block_node.children[0]
220
+ end
221
+
222
+ def module_creation?(node)
223
+ # "Class.new"
224
+ # "Module.new"
225
+ type(node) == METHOD_CALL &&
226
+ type(receiver(node)) == CONSTANT &&
227
+ ["Class", "Module"].include?(constant_name(receiver(node))) &&
228
+ method_name(node) == :new
229
+ end
230
+
231
+ def name_from_block_definition(node)
232
+ if method_name(method_call_node(node)) == :class_eval
233
+ receiver = receiver(node)
234
+ constant_name(receiver) if receiver && type(receiver) == CONSTANT
235
+ end
236
+ end
237
+
238
+ def name_part_from_definition(node)
239
+ case type(node)
240
+ when CLASS, MODULE, CONSTANT_ASSIGNMENT
241
+ module_name_from_definition(node)
242
+ when BLOCK
243
+ name_from_block_definition(node)
244
+ end
245
+ end
246
+
247
+ def receiver(method_call_or_block_node)
248
+ case type(method_call_or_block_node)
249
+ when METHOD_CALL
250
+ method_call_or_block_node.children[0]
251
+ when BLOCK
252
+ receiver(method_call_node(method_call_or_block_node))
253
+ else
254
+ raise TypeError
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end