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.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
- data/.github/probots.yml +2 -0
- data/.github/pull_request_template.md +27 -0
- data/.github/workflows/ci.yml +50 -0
- data/.gitignore +12 -0
- data/.rubocop.yml +46 -0
- data/.ruby-version +1 -0
- data/CODEOWNERS +1 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/CONTRIBUTING.md +17 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +236 -0
- data/LICENSE.md +7 -0
- data/README.md +73 -0
- data/Rakefile +13 -0
- data/TROUBLESHOOT.md +67 -0
- data/USAGE.md +250 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/dev.yml +32 -0
- data/docs/cohesion.png +0 -0
- data/exe/packwerk +6 -0
- data/lib/packwerk.rb +44 -0
- data/lib/packwerk/application_validator.rb +343 -0
- data/lib/packwerk/association_inspector.rb +44 -0
- data/lib/packwerk/checking_deprecated_references.rb +40 -0
- data/lib/packwerk/cli.rb +238 -0
- data/lib/packwerk/configuration.rb +82 -0
- data/lib/packwerk/const_node_inspector.rb +44 -0
- data/lib/packwerk/constant_discovery.rb +60 -0
- data/lib/packwerk/constant_name_inspector.rb +22 -0
- data/lib/packwerk/dependency_checker.rb +28 -0
- data/lib/packwerk/deprecated_references.rb +92 -0
- data/lib/packwerk/file_processor.rb +43 -0
- data/lib/packwerk/files_for_processing.rb +67 -0
- data/lib/packwerk/formatters/progress_formatter.rb +46 -0
- data/lib/packwerk/generators/application_validation.rb +62 -0
- data/lib/packwerk/generators/configuration_file.rb +69 -0
- data/lib/packwerk/generators/inflections_file.rb +43 -0
- data/lib/packwerk/generators/root_package.rb +37 -0
- data/lib/packwerk/generators/templates/inflections.yml +6 -0
- data/lib/packwerk/generators/templates/package.yml +17 -0
- data/lib/packwerk/generators/templates/packwerk +23 -0
- data/lib/packwerk/generators/templates/packwerk.yml.erb +23 -0
- data/lib/packwerk/generators/templates/packwerk_validator_test.rb +11 -0
- data/lib/packwerk/graph.rb +74 -0
- data/lib/packwerk/inflections/custom.rb +33 -0
- data/lib/packwerk/inflections/default.rb +73 -0
- data/lib/packwerk/inflector.rb +41 -0
- data/lib/packwerk/node.rb +259 -0
- data/lib/packwerk/node_processor.rb +49 -0
- data/lib/packwerk/node_visitor.rb +22 -0
- data/lib/packwerk/offense.rb +44 -0
- data/lib/packwerk/output_styles.rb +41 -0
- data/lib/packwerk/package.rb +56 -0
- data/lib/packwerk/package_set.rb +59 -0
- data/lib/packwerk/parsed_constant_definitions.rb +62 -0
- data/lib/packwerk/parsers.rb +23 -0
- data/lib/packwerk/parsers/erb.rb +66 -0
- data/lib/packwerk/parsers/factory.rb +34 -0
- data/lib/packwerk/parsers/ruby.rb +42 -0
- data/lib/packwerk/privacy_checker.rb +45 -0
- data/lib/packwerk/reference.rb +6 -0
- data/lib/packwerk/reference_extractor.rb +81 -0
- data/lib/packwerk/reference_lister.rb +23 -0
- data/lib/packwerk/run_context.rb +103 -0
- data/lib/packwerk/sanity_checker.rb +10 -0
- data/lib/packwerk/spring_command.rb +28 -0
- data/lib/packwerk/updating_deprecated_references.rb +51 -0
- data/lib/packwerk/version.rb +6 -0
- data/lib/packwerk/violation_type.rb +13 -0
- data/library.yml +6 -0
- data/packwerk.gemspec +58 -0
- data/service.yml +6 -0
- data/shipit.rubygems.yml +1 -0
- data/sorbet/config +2 -0
- data/sorbet/rbi/gems/actioncable@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +840 -0
- data/sorbet/rbi/gems/actionmailbox@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +571 -0
- data/sorbet/rbi/gems/actionmailer@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +568 -0
- data/sorbet/rbi/gems/actionpack@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +5216 -0
- data/sorbet/rbi/gems/actiontext@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +663 -0
- data/sorbet/rbi/gems/actionview@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +2504 -0
- data/sorbet/rbi/gems/activejob@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +635 -0
- data/sorbet/rbi/gems/activemodel@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +1201 -0
- data/sorbet/rbi/gems/activerecord@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +8011 -0
- data/sorbet/rbi/gems/activestorage@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +904 -0
- data/sorbet/rbi/gems/activesupport@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +3888 -0
- data/sorbet/rbi/gems/ast@2.4.1.rbi +54 -0
- data/sorbet/rbi/gems/better_html@1.0.15.rbi +317 -0
- data/sorbet/rbi/gems/builder@3.2.4.rbi +8 -0
- data/sorbet/rbi/gems/byebug@11.1.3.rbi +8 -0
- data/sorbet/rbi/gems/coderay@1.1.3.rbi +8 -0
- data/sorbet/rbi/gems/colorize@0.8.1.rbi +40 -0
- data/sorbet/rbi/gems/commander@4.5.2.rbi +8 -0
- data/sorbet/rbi/gems/concurrent-ruby@1.1.6.rbi +1966 -0
- data/sorbet/rbi/gems/constant_resolver@0.1.5.rbi +26 -0
- data/sorbet/rbi/gems/crass@1.0.6.rbi +138 -0
- data/sorbet/rbi/gems/erubi@1.9.0.rbi +39 -0
- data/sorbet/rbi/gems/globalid@0.4.2.rbi +178 -0
- data/sorbet/rbi/gems/highline@2.0.3.rbi +8 -0
- data/sorbet/rbi/gems/html_tokenizer@0.0.7.rbi +46 -0
- data/sorbet/rbi/gems/i18n@1.8.2.rbi +633 -0
- data/sorbet/rbi/gems/jaro_winkler@1.5.4.rbi +8 -0
- data/sorbet/rbi/gems/loofah@2.5.0.rbi +272 -0
- data/sorbet/rbi/gems/m@1.5.1.rbi +108 -0
- data/sorbet/rbi/gems/mail@2.7.1.rbi +2490 -0
- data/sorbet/rbi/gems/marcel@0.3.3.rbi +30 -0
- data/sorbet/rbi/gems/method_source@1.0.0.rbi +76 -0
- data/sorbet/rbi/gems/mimemagic@0.3.5.rbi +47 -0
- data/sorbet/rbi/gems/mini_mime@1.0.2.rbi +71 -0
- data/sorbet/rbi/gems/mini_portile2@2.4.0.rbi +8 -0
- data/sorbet/rbi/gems/minitest@5.14.0.rbi +542 -0
- data/sorbet/rbi/gems/mocha@1.11.2.rbi +964 -0
- data/sorbet/rbi/gems/nio4r@2.5.2.rbi +89 -0
- data/sorbet/rbi/gems/nokogiri@1.10.9.rbi +1608 -0
- data/sorbet/rbi/gems/parallel@1.19.1.rbi +8 -0
- data/sorbet/rbi/gems/parlour@4.0.1.rbi +561 -0
- data/sorbet/rbi/gems/parser@2.7.1.4.rbi +1632 -0
- data/sorbet/rbi/gems/pry@0.13.1.rbi +8 -0
- data/sorbet/rbi/gems/rack-test@1.1.0.rbi +335 -0
- data/sorbet/rbi/gems/rack@2.2.2.rbi +1730 -0
- data/sorbet/rbi/gems/rails-dom-testing@2.0.3.rbi +123 -0
- data/sorbet/rbi/gems/rails-html-sanitizer@1.3.0.rbi +213 -0
- data/sorbet/rbi/gems/rails@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +8 -0
- data/sorbet/rbi/gems/railties@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +869 -0
- data/sorbet/rbi/gems/rainbow@3.0.0.rbi +155 -0
- data/sorbet/rbi/gems/rake@13.0.1.rbi +841 -0
- data/sorbet/rbi/gems/rexml@3.2.4.rbi +8 -0
- data/sorbet/rbi/gems/rubocop-performance@1.5.2.rbi +8 -0
- data/sorbet/rbi/gems/rubocop-shopify@1.0.2.rbi +8 -0
- data/sorbet/rbi/gems/rubocop-sorbet@0.3.7.rbi +8 -0
- data/sorbet/rbi/gems/rubocop@0.82.0.rbi +8 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.10.1.rbi +8 -0
- data/sorbet/rbi/gems/smart_properties@1.15.0.rbi +168 -0
- data/sorbet/rbi/gems/spoom@1.0.4.rbi +418 -0
- data/sorbet/rbi/gems/spring@2.1.0.rbi +160 -0
- data/sorbet/rbi/gems/sprockets-rails@3.2.1.rbi +431 -0
- data/sorbet/rbi/gems/sprockets@4.0.0.rbi +1132 -0
- data/sorbet/rbi/gems/tapioca@0.4.5.rbi +518 -0
- data/sorbet/rbi/gems/thor@1.0.1.rbi +892 -0
- data/sorbet/rbi/gems/tzinfo@2.0.2.rbi +547 -0
- data/sorbet/rbi/gems/unicode-display_width@1.7.0.rbi +8 -0
- data/sorbet/rbi/gems/websocket-driver@0.7.1.rbi +438 -0
- data/sorbet/rbi/gems/websocket-extensions@0.1.4.rbi +71 -0
- data/sorbet/rbi/gems/zeitwerk@2.3.0.rbi +8 -0
- data/sorbet/tapioca/require.rb +25 -0
- data/static/packwerk-check-demo.png +0 -0
- data/static/packwerk_check.gif +0 -0
- data/static/packwerk_check_violation.gif +0 -0
- data/static/packwerk_update.gif +0 -0
- data/static/packwerk_validate.gif +0 -0
- 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,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
|