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