packwerk 1.0.0

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