├── .ruby-version
├── sorbet
├── config
├── rbi
│ └── gems
│ │ ├── pry@0.14.0.rbi
│ │ ├── rexml@3.2.4.rbi
│ │ ├── rubocop@1.12.0.rbi
│ │ ├── builder@3.2.4.rbi
│ │ ├── byebug@11.1.3.rbi
│ │ ├── coderay@1.1.3.rbi
│ │ ├── highline@2.0.3.rbi
│ │ ├── commander@4.5.2.rbi
│ │ ├── regexp_parser@2.1.1.rbi
│ │ ├── rubocop-ast@1.4.1.rbi
│ │ ├── minitest-focus@1.2.1.rbi
│ │ ├── rubocop-shopify@2.0.1.rbi
│ │ ├── rubocop-sorbet@0.6.1.rbi
│ │ ├── ruby-progressbar@1.11.0.rbi
│ │ ├── rubocop-performance@1.10.2.rbi
│ │ ├── unicode-display_width@2.0.0.rbi
│ │ ├── rails@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi
│ │ ├── constant_resolver@0.1.5.rbi
│ │ ├── erubi@1.10.0.rbi
│ │ ├── colorize@0.8.1.rbi
│ │ ├── html_tokenizer@0.0.7.rbi
│ │ ├── ast@2.4.2.rbi
│ │ ├── mini_mime@1.0.3.rbi
│ │ ├── racc@1.5.2.rbi
│ │ ├── marcel@1.0.0.rbi
│ │ ├── websocket-extensions@0.1.5.rbi
│ │ ├── nio4r@2.5.7.rbi
│ │ ├── method_source@1.0.0.rbi
│ │ ├── m@1.5.1.rbi
│ │ ├── parallel@1.20.1.rbi
│ │ ├── rails-dom-testing@2.0.3.rbi
│ │ ├── rainbow@3.0.0.rbi
│ │ └── spring@2.1.1.rbi
└── tapioca
│ └── require.rb
├── test
├── fixtures
│ ├── blank
│ │ ├── package.yml
│ │ └── packwerk.yml
│ ├── minimal
│ │ ├── package.yml
│ │ ├── config
│ │ │ └── environment.rb
│ │ ├── packwerk.yml
│ │ └── components
│ │ │ └── sales
│ │ │ ├── app
│ │ │ ├── models
│ │ │ │ ├── order.rb
│ │ │ │ └── sales
│ │ │ │ │ └── order.rb
│ │ │ └── public
│ │ │ │ └── sales
│ │ │ │ └── record_new_order.rb
│ │ │ └── package.yml
│ ├── skeleton
│ │ ├── config
│ │ │ ├── .gitkeep
│ │ │ └── environment.rb
│ │ ├── package.yml
│ │ ├── components
│ │ │ ├── platform
│ │ │ │ └── app
│ │ │ │ │ └── models
│ │ │ │ │ └── .gitkeep
│ │ │ ├── timeline
│ │ │ │ ├── nested
│ │ │ │ │ └── package.yml
│ │ │ │ ├── app
│ │ │ │ │ └── models
│ │ │ │ │ │ ├── sales
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ │ ├── imaginary
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ │ ├── private_thing.rb
│ │ │ │ │ │ └── concerns
│ │ │ │ │ │ └── has_timeline.rb
│ │ │ │ └── package.yml
│ │ │ └── sales
│ │ │ │ ├── app
│ │ │ │ ├── views
│ │ │ │ │ └── order.html.erb
│ │ │ │ ├── models
│ │ │ │ │ ├── order.rb
│ │ │ │ │ ├── sales
│ │ │ │ │ │ ├── entry.rb
│ │ │ │ │ │ ├── order.rb
│ │ │ │ │ │ ├── temp.rb
│ │ │ │ │ │ └── order
│ │ │ │ │ │ │ └── error.rb
│ │ │ │ │ ├── payment_details.rb
│ │ │ │ │ └── order
│ │ │ │ │ │ └── extension.rb
│ │ │ │ └── public
│ │ │ │ │ └── sales
│ │ │ │ │ ├── record_new_order.rb
│ │ │ │ │ └── errors.rb
│ │ │ │ ├── test
│ │ │ │ └── unit
│ │ │ │ │ └── order_test.rb
│ │ │ │ └── package.yml
│ │ ├── vendor
│ │ │ └── cache
│ │ │ │ └── gems
│ │ │ │ └── example
│ │ │ │ ├── package.yml
│ │ │ │ └── models
│ │ │ │ └── .gitkeep
│ │ ├── custom_inflections.yml
│ │ └── packwerk.yml
│ ├── formats
│ │ ├── ruby
│ │ │ ├── valid.rb
│ │ │ ├── invalid.rb
│ │ │ └── invalid_utf8_string.rb
│ │ └── erb
│ │ │ ├── invalid.erb
│ │ │ └── valid.erb
│ ├── deprecated_references.yml
│ └── deprecated_references_with_conflicts.yml
├── parser_test_helper.rb
├── support
│ ├── test_assertions.rb
│ ├── test_macro.rb
│ ├── factory_helper.rb
│ ├── yaml_file.rb
│ ├── rails_paths.rb
│ ├── rails_application_fixture_helper.rb
│ └── application_fixture_helper.rb
├── unit
│ ├── offense_test.rb
│ ├── node_visitor_test.rb
│ ├── generators
│ │ ├── root_package_test.rb
│ │ ├── inflections_file_test.rb
│ │ └── configuration_file_test.rb
│ ├── formatters
│ │ ├── offenses_formatter_test.rb
│ │ └── progress_formatter_test.rb
│ ├── dependency_checker_test.rb
│ ├── graph_test.rb
│ ├── reference_offense_test.rb
│ ├── parsers
│ │ ├── factory_test.rb
│ │ ├── erb_test.rb
│ │ └── ruby_test.rb
│ ├── inflector_test.rb
│ ├── constant_discovery_test.rb
│ ├── inflections
│ │ └── custom_test.rb
│ ├── offense_collection_test.rb
│ ├── association_inspector_test.rb
│ ├── files_for_processing_test.rb
│ ├── configuration_test.rb
│ ├── package_set_test.rb
│ ├── privacy_checker_test.rb
│ └── application_load_paths_test.rb
├── rails_test_helper.rb
├── test_helper.rb
├── integration
│ └── offense_collection_test.rb
└── const_node_inspector_test.rb
├── CODEOWNERS
├── .github
├── probots.yml
├── ISSUE_TEMPLATE
│ └── bug_report.md
├── pull_request_template.md
└── workflows
│ └── ci.yml
├── service.yml
├── docs
└── cohesion.png
├── static
├── packwerk_check.gif
├── packwerk_update.gif
├── packwerk_validate.gif
├── packwerk-check-demo.png
└── packwerk_check_violation.gif
├── lib
├── packwerk
│ ├── version.rb
│ ├── reference.rb
│ ├── result.rb
│ ├── violation_type.rb
│ ├── generators
│ │ ├── templates
│ │ │ ├── inflections.yml
│ │ │ ├── packwerk.yml.erb
│ │ │ └── package.yml
│ │ ├── root_package.rb
│ │ ├── inflections_file.rb
│ │ └── configuration_file.rb
│ ├── sanity_checker.rb
│ ├── checker.rb
│ ├── output_style.rb
│ ├── parsers.rb
│ ├── offenses_formatter.rb
│ ├── node_visitor.rb
│ ├── output_styles
│ │ ├── plain.rb
│ │ └── coloured.rb
│ ├── constant_name_inspector.rb
│ ├── dependency_checker.rb
│ ├── offense.rb
│ ├── parsers
│ │ ├── factory.rb
│ │ ├── ruby.rb
│ │ └── erb.rb
│ ├── node_processor_factory.rb
│ ├── inflections
│ │ ├── custom.rb
│ │ └── default.rb
│ ├── inflector.rb
│ ├── file_processor.rb
│ ├── node_processor.rb
│ ├── formatters
│ │ ├── progress_formatter.rb
│ │ └── offenses_formatter.rb
│ ├── privacy_checker.rb
│ ├── package.rb
│ ├── files_for_processing.rb
│ ├── configuration.rb
│ ├── const_node_inspector.rb
│ ├── package_set.rb
│ ├── graph.rb
│ ├── reference_offense.rb
│ ├── parsed_constant_definitions.rb
│ ├── association_inspector.rb
│ ├── reference_extractor.rb
│ ├── offense_collection.rb
│ ├── constant_discovery.rb
│ ├── application_load_paths.rb
│ ├── run_context.rb
│ └── parse_run.rb
└── packwerk.rb
├── shipit.rubygems.yml
├── library.yml
├── bin
├── setup
├── console
├── m
├── rake
├── srb
├── rubocop
└── tapioca
├── exe
└── packwerk
├── README.md
├── .gitignore
├── Rakefile
├── gemfiles
└── Gemfile-rails-6-0
├── Gemfile
├── dev.yml
├── .rubocop.yml
├── LICENSE.md
├── CONTRIBUTING.md
├── packwerk.gemspec
└── CODE_OF_CONDUCT.md
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.4.9
2 |
--------------------------------------------------------------------------------
/sorbet/config:
--------------------------------------------------------------------------------
1 | --dir
2 | .
3 |
--------------------------------------------------------------------------------
/test/fixtures/blank/package.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/blank/packwerk.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/minimal/package.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @Shopify/packwerk
2 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/config/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/package.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/minimal/config/environment.rb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/probots.yml:
--------------------------------------------------------------------------------
1 | enabled:
2 | - cla
3 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/platform/app/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/timeline/nested/package.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/vendor/cache/gems/example/package.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/timeline/app/models/sales/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/vendor/cache/gems/example/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/timeline/app/models/imaginary/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/service.yml:
--------------------------------------------------------------------------------
1 | classification: library
2 | slack_channels:
3 | - core-stewardship
4 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/sales/app/views/order.html.erb:
--------------------------------------------------------------------------------
1 |
Order
2 |
--------------------------------------------------------------------------------
/docs/cohesion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workato/packwerk-fork/main/docs/cohesion.png
--------------------------------------------------------------------------------
/test/fixtures/formats/ruby/valid.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 |
3 | def method
4 | puts 'test'
5 | end
6 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/timeline/app/models/private_thing.rb:
--------------------------------------------------------------------------------
1 | class PrivateThing
2 | end
3 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/timeline/package.yml:
--------------------------------------------------------------------------------
1 | enforce_privacy:
2 | - "::PrivateThing"
3 |
--------------------------------------------------------------------------------
/static/packwerk_check.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workato/packwerk-fork/main/static/packwerk_check.gif
--------------------------------------------------------------------------------
/static/packwerk_update.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workato/packwerk-fork/main/static/packwerk_update.gif
--------------------------------------------------------------------------------
/static/packwerk_validate.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workato/packwerk-fork/main/static/packwerk_validate.gif
--------------------------------------------------------------------------------
/test/fixtures/formats/ruby/invalid.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 |
3 | def end_misspelled
4 | puts 'uh oh'
5 | edn
6 |
--------------------------------------------------------------------------------
/static/packwerk-check-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workato/packwerk-fork/main/static/packwerk-check-demo.png
--------------------------------------------------------------------------------
/test/fixtures/formats/ruby/invalid_utf8_string.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 |
3 | def method
4 | puts "\xff"
5 | end
6 |
--------------------------------------------------------------------------------
/test/fixtures/minimal/packwerk.yml:
--------------------------------------------------------------------------------
1 | include:
2 | - "**/*.rb"
3 | load_paths:
4 | - components/sales/app/models
5 |
--------------------------------------------------------------------------------
/static/packwerk_check_violation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/workato/packwerk-fork/main/static/packwerk_check_violation.gif
--------------------------------------------------------------------------------
/lib/packwerk/version.rb:
--------------------------------------------------------------------------------
1 | # typed: strong
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | VERSION = "1.3.2"
6 | end
7 |
--------------------------------------------------------------------------------
/shipit.rubygems.yml:
--------------------------------------------------------------------------------
1 | dependencies:
2 | override: []
3 | deploy:
4 | override:
5 | - release-gem packwerk.gemspec rake release
6 |
--------------------------------------------------------------------------------
/library.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Packwerk
3 | stewards:
4 | - "@Shopify/kernel-architecture-patterns"
5 | slack_channels:
6 | - code-foundations
7 |
--------------------------------------------------------------------------------
/test/fixtures/minimal/components/sales/app/models/order.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | class Order
5 | end
6 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/sales/app/models/order.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | class Order
5 | end
6 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/sales/app/models/sales/entry.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | class Entry
5 | end
6 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/sales/test/unit/order_test.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | class OrderTest
5 | end
6 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/sales/app/models/payment_details.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | class PaymentDetails; end
5 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/timeline/app/models/concerns/has_timeline.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | module HasTimeline
5 | end
6 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/test/fixtures/formats/erb/invalid.erb:
--------------------------------------------------------------------------------
1 | some test
2 | <%= function_call %>
3 | <% if condition %>
4 | `if` without an `end`
5 |
6 | <% @stuff.each do { test } end %>
7 |
--------------------------------------------------------------------------------
/test/fixtures/minimal/components/sales/app/models/sales/order.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | module Sales
5 | class Order
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/sales/app/models/sales/order.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | module Sales
5 | class Order
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/sales/app/models/sales/temp.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | module Sales
5 | class Temp
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/packwerk/reference.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | Reference = Struct.new(:source_package, :relative_path, :constant)
6 | end
7 |
--------------------------------------------------------------------------------
/test/fixtures/minimal/components/sales/package.yml:
--------------------------------------------------------------------------------
1 | ---
2 | enforce_privacy: true
3 |
4 | metadata:
5 | stewards:
6 | - "@Shopify/sales"
7 | slack_channels:
8 | - "#sales"
9 |
--------------------------------------------------------------------------------
/exe/packwerk:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "packwerk"
5 |
6 | Packwerk::Cli.new(style: Packwerk::OutputStyles::Coloured.new).run(ARGV.dup)
7 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/sales/app/models/order/extension.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | class Order
5 | class Extension
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/fixtures/minimal/components/sales/app/public/sales/record_new_order.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | module Sales
5 | module RecordNewOrder
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/sales/app/public/sales/record_new_order.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | module Sales
5 | module RecordNewOrder
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/sales/app/models/sales/order/error.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | module Sales
5 | class Order
6 | class Error; end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/packwerk/result.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | class Result < T::Struct
6 | prop :message, String
7 | prop :status, T::Boolean
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The repo is a fork of https://github.com/Shopify/packwerk
2 | ## Motivation
3 | The mainstream repo uses Ruby 3.0.0
4 |
5 | Forked to support ruby 2.4
6 |
7 |
8 | ## Test passes
9 | ```
10 |
11 |
12 | ```
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/sales/app/public/sales/errors.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | module Sales
5 | module Errors
6 | SomethingWentWrong = Class.new(RuntimeError)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/components/sales/package.yml:
--------------------------------------------------------------------------------
1 | ---
2 | enforce_privacy: true
3 | dependencies:
4 | - 'components/timeline'
5 |
6 | metadata:
7 | stewards:
8 | - "@Shopify/sales"
9 | slack_channels:
10 | - "#sales"
11 |
--------------------------------------------------------------------------------
/lib/packwerk/violation_type.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | class ViolationType < T::Enum
6 | enums do
7 | Privacy = new
8 | Dependency = new
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /.bundle/
3 | /.yardoc
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 | /.bundle/
11 | /tmp/
12 | .rubocop-*
13 | .byebug_history
14 | sorbet/rbi/hidden-definitions/errors.txt
15 | .rakeTasks
--------------------------------------------------------------------------------
/test/parser_test_helper.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | module ParserTestHelper
5 | class << self
6 | def parse(source)
7 | Packwerk::Parsers::Ruby.new.call(io: StringIO.new(source))
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/config/environment.rb:
--------------------------------------------------------------------------------
1 | require "packwerk/inflections/custom"
2 |
3 | ActiveSupport::Inflector.inflections do |inflect|
4 | Packwerk::Inflections::Custom.new(
5 | Rails.root.join("custom_inflections.yml")
6 | ).apply_to(inflect)
7 | end
8 |
--------------------------------------------------------------------------------
/lib/packwerk/generators/templates/inflections.yml:
--------------------------------------------------------------------------------
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"
7 |
--------------------------------------------------------------------------------
/test/fixtures/deprecated_references.yml:
--------------------------------------------------------------------------------
1 | ---
2 | buyers:
3 | "::Buyers::Document":
4 | violations:
5 | - dependency
6 | files:
7 | - orders/app/jobs/orders/sweepers/purge_old_document_rows_task.rb
8 | - orders/app/models/orders/services/adjustment_engine.rb
9 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/custom_inflections.yml:
--------------------------------------------------------------------------------
1 | acronym:
2 | - 'GraphQL'
3 | - 'MRuby'
4 | - 'TOS'
5 | irregular:
6 | - ['analysis', 'analyses']
7 | - ['reserve', 'reserves']
8 | uncountable:
9 | - 'payment_details'
10 | singular:
11 | - [!ruby/regexp /status$/, 'status']
12 |
--------------------------------------------------------------------------------
/test/support/test_assertions.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 | module TestAssertions
4 | def self.included(klass)
5 | # byebug
6 | # klass.alias(:assert_not_nil :refute_nil)
7 | klass.class_eval { alias assert_not_nil refute_nil }
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require "rake/testtask"
5 |
6 | Rake::TestTask.new(:test) do |t|
7 | t.libs << "test"
8 | t.libs << "lib"
9 | t.test_files = FileList["test/**/*_test.rb"]
10 | t.warning = true
11 | end
12 |
13 | task(default: :test)
14 |
--------------------------------------------------------------------------------
/test/fixtures/deprecated_references_with_conflicts.yml:
--------------------------------------------------------------------------------
1 | ---
2 | buyers:
3 | "::Buyers::Document":
4 | violations:
5 | - dependency
6 | files:
7 | <<<< HEAD
8 | - orders/app/jobs/orders/sweepers/purge_old_document_rows_task.rb
9 | ====
10 | >>>> Commit
11 | - orders/app/models/orders/services/adjustment_engine.rb
12 |
--------------------------------------------------------------------------------
/lib/packwerk/sanity_checker.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | # To do: This alias and file should be removed as it is deprecated
6 | warn("DEPRECATION WARNING: Packwerk::SanityChecker is deprecated, use Packwerk::ApplicationValidator instead.")
7 | SanityChecker = ApplicationValidator
8 | end
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/pry@0.14.0.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `pry` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/rexml@3.2.4.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `rexml` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/rubocop@1.12.0.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `rubocop` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/builder@3.2.4.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `builder` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/byebug@11.1.3.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `byebug` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/coderay@1.1.3.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `coderay` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/highline@2.0.3.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `highline` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/commander@4.5.2.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `commander` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/regexp_parser@2.1.1.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `regexp_parser` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/rubocop-ast@1.4.1.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `rubocop-ast` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/minitest-focus@1.2.1.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `minitest-focus` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/rubocop-shopify@2.0.1.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `rubocop-shopify` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/rubocop-sorbet@0.6.1.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `rubocop-sorbet` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `ruby-progressbar` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/rubocop-performance@1.10.2.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `rubocop-performance` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/unicode-display_width@2.0.0.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `unicode-display_width` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/rails@7.0.0.alpha-d612542336d9a61381311c95a27d801bb4094779.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `rails` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | # THIS IS AN EMPTY RBI FILE.
8 | # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
9 |
--------------------------------------------------------------------------------
/lib/packwerk/checker.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | module Checker
6 | extend T::Sig
7 | extend T::Helpers
8 |
9 | interface!
10 |
11 | sig { returns(ViolationType).abstract }
12 | def violation_type; end
13 |
14 | sig { params(reference: Reference).returns(T::Boolean).abstract }
15 | def invalid_reference?(reference); end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/support/test_macro.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | module TestMacro
5 | def test(description, &block)
6 | method_name = "test_#{description}".gsub(/\W/, "_")
7 | define_method(method_name, &block)
8 | end
9 |
10 | def setup(&block)
11 | define_method(:setup, &block)
12 | end
13 |
14 | def teardown(&block)
15 | define_method(:teardown, &block)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/packwerk/output_style.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | module OutputStyle
6 | extend T::Sig
7 | extend T::Helpers
8 |
9 | interface!
10 |
11 | sig { abstract.returns(String) }
12 | def reset; end
13 |
14 | sig { abstract.returns(String) }
15 | def filename; end
16 |
17 | sig { abstract.returns(String) }
18 | def error; end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/fixtures/skeleton/packwerk.yml:
--------------------------------------------------------------------------------
1 | include:
2 | - "**/*.rb"
3 | - "**/*.{blop,rb}" # To test for duplicated files
4 | exclude:
5 | - "**/temp.rb"
6 | load_paths:
7 | - components/platform/app/models
8 | - components/sales/app/models
9 | - components/timeline/app/models
10 | - components/timeline/app/models/concerns
11 | - vendor/cache/gems/example/models
12 | inflections_file: "custom_inflections.yml"
13 | parallel: false
14 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "bundler/setup"
5 | require "packwerk"
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | # (If you use this, don't forget to add pry to your Gemfile!)
11 | # require "pry"
12 | # Pry.start
13 |
14 | require "irb"
15 | IRB.start(__FILE__)
16 |
--------------------------------------------------------------------------------
/lib/packwerk/parsers.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | module Parsers
6 | autoload :Erb, "packwerk/parsers/erb"
7 | autoload :Factory, "packwerk/parsers/factory"
8 | autoload :Ruby, "packwerk/parsers/ruby"
9 |
10 | class ParseResult < Offense; end
11 |
12 | class ParseError < StandardError
13 | attr_reader :result
14 |
15 | def initialize(result)
16 | super(result.message)
17 | @result = result
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/packwerk/offenses_formatter.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | module OffensesFormatter
6 | extend T::Sig
7 | extend T::Helpers
8 |
9 | interface!
10 |
11 | sig { abstract.params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
12 | def show_offenses(offenses)
13 | end
14 |
15 | sig { abstract.params(offense_collection: Packwerk::OffenseCollection).returns(String) }
16 | def show_stale_violations(offense_collection)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/packwerk/node_visitor.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | class NodeVisitor
6 | def initialize(node_processor:)
7 | @node_processor = node_processor
8 | end
9 |
10 | def visit(node, ancestors:, result:)
11 | result.concat(@node_processor.call(node, ancestors))
12 |
13 | child_ancestors = [node] + ancestors
14 | Node.each_child(node) do |child|
15 | visit(child, ancestors: child_ancestors, result: result)
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/packwerk/output_styles/plain.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | module OutputStyles
6 | class Plain
7 | extend T::Sig
8 | include OutputStyle
9 |
10 | sig { override.returns(String) }
11 | def reset
12 | ""
13 | end
14 |
15 | sig { override.returns(String) }
16 | def filename
17 | ""
18 | end
19 |
20 | sig { override.returns(String) }
21 | def error
22 | ""
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/packwerk/constant_name_inspector.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "ast"
5 |
6 | module Packwerk
7 | # An interface describing some object that can extract a constant name from an AST node
8 | module ConstantNameInspector
9 | extend T::Sig
10 | extend T::Helpers
11 |
12 | interface!
13 |
14 | sig do
15 | params(node: ::AST::Node, ancestors: T::Array[::AST::Node])
16 | .returns(T.nilable(String))
17 | .abstract
18 | end
19 | def constant_name_from_node(node, ancestors:); end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/gemfiles/Gemfile-rails-6-0:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source("https://rubygems.org")
4 |
5 | gemspec path: ".."
6 |
7 | # Specify the same dependency sources as the application Gemfile
8 |
9 | gem("spring")
10 | gem("rails", '~> 6.0.0')
11 | gem("constant_resolver", require: false)
12 | gem("sorbet-runtime", require: false)
13 | gem("rubocop-performance", require: false)
14 | gem("rubocop-sorbet", require: false)
15 | gem("mocha", require: false)
16 | gem("rubocop-shopify", require: false)
17 | gem("tapioca", require: false)
18 |
19 | group :development do
20 | gem("byebug", require: false)
21 | gem("minitest-focus", require: false)
22 | end
23 |
--------------------------------------------------------------------------------
/test/unit/offense_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | class OffenseTest < Minitest::Test
8 | setup do
9 | location = Node::Location.new(90, 10)
10 | file = "components/platform/shop.rb"
11 | message = "Violation of developer rights"
12 | @offense = Offense.new(location: location, file: file, message: message)
13 | end
14 |
15 | test "#to_s returns the location of offense and message" do
16 | expected_message = "components/platform/shop.rb:90:10\nViolation of developer rights\n"
17 | assert_equal(expected_message, @offense.to_s)
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/support/factory_helper.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | module FactoryHelper
5 | def build_reference(
6 | source_package: Packwerk::Package.new(name: "components/source", config: {}),
7 | destination_package: Packwerk::Package.new(name: "components/destination", config: {}),
8 | path: "some/path.rb",
9 | constant_name: "::SomeName",
10 | public_constant: false
11 | )
12 | constant = Packwerk::ConstantDiscovery::ConstantContext.new(
13 | constant_name,
14 | "some/location.rb",
15 | destination_package,
16 | public_constant
17 | )
18 | Packwerk::Reference.new(source_package, path, constant)
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/unit/node_visitor_test.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 | require "parser_test_helper"
6 |
7 | module Packwerk
8 | class NodeVisitorTest < Minitest::Test
9 | test "#visit visits the correct number of nodes" do
10 | node_processor = mock
11 | node_processor.expects(:call).times(3).returns(["an offense"])
12 | file_node_visitor = Packwerk::NodeVisitor.new(node_processor: node_processor)
13 |
14 | node = ParserTestHelper.parse("class Hello; world; end")
15 | result = []
16 | file_node_visitor.visit(node, ancestors: [], result: result)
17 |
18 | assert_equal 3, result.count
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/rails_test_helper.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "rails"
5 |
6 | class Dummy < Rails::Application
7 | def self.skeleton(*path)
8 | ROOT.join("test", "fixtures", "skeleton", *path).to_s
9 | end
10 |
11 | config.eager_load_paths = [skeleton("components", "platform", "app", "models")]
12 | config.autoload_paths = [skeleton("components", "sales", "app", "models")]
13 | config.autoload_once_paths = [
14 | skeleton("components", "timeline", "app", "models"),
15 | skeleton("components", "timeline", "app", "models", "concerns"),
16 | skeleton("vendor", "cache", "gems", "example", "models"),
17 | ]
18 | config.root = skeleton(".")
19 | end
20 |
--------------------------------------------------------------------------------
/lib/packwerk/output_styles/coloured.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | module OutputStyles
6 | # See https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit for ANSI escape colour codes
7 | class Coloured
8 | extend T::Sig
9 | include OutputStyle
10 |
11 | sig { override.returns(String) }
12 | def reset
13 | "\033[m"
14 | end
15 |
16 | sig { override.returns(String) }
17 | def filename
18 | # 36 is foreground cyan
19 | "\033[36m"
20 | end
21 |
22 | sig { override.returns(String) }
23 | def error
24 | # 31 is foreground red
25 | "\033[31m"
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug / Issue Report
3 | about: Create a report to help us improve
4 | title: '[Bug Report]'
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Description**
11 | A clear and concise description of what the bug/issue is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior.
15 |
16 | **Expected Behaviour**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Screenshots**
20 | If applicable, add screenshots to help explain your problem.
21 |
22 | **Version Information**
23 | - Packwerk: [e.g. v0.1.7]
24 | - Ruby [e.g. v1.7]
25 |
26 | **Additional Context**
27 | How have you tried to solve the issue? Add any other context about the problem here.
28 |
--------------------------------------------------------------------------------
/sorbet/tapioca/require.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # DO NOT EDIT MANUALLY
3 | # This is an autogenerated file for explicit gem requires.
4 | # Please instead update this file by running `tapioca require`.
5 |
6 | # typed: false
7 |
8 | require "ast"
9 | require "ast/node"
10 | require "benchmark"
11 | require "better_html"
12 | require "better_html/parser"
13 | require "constant_resolver"
14 | require "minitest/autorun"
15 | require "mocha/minitest"
16 | require "parallel"
17 | require "parser"
18 | require "parser/ast/node"
19 | require "parser/current"
20 | require "parser/source/buffer"
21 | require "parser/source/map"
22 | require "pathname"
23 | require "rails/all"
24 | require "singleton"
25 | require "spring/commands"
26 | require "yaml"
27 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source("https://rubygems.org")
4 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}.git" }
5 |
6 | gemspec
7 |
8 | # Specify the same dependency sources as the application Gemfile
9 |
10 | gem("spring")
11 | gem("rails", "~> 5.2.4")
12 | gem("constant_resolver", git: "https://github.com/novokshonovp/constant_resolver", require: false)
13 | gem("sorbet-runtime", require: false)
14 | gem("rubocop-performance", require: false)
15 | gem("rubocop-sorbet", require: false)
16 | gem("mocha", require: false)
17 | gem("rubocop-shopify", require: false)
18 | gem("tapioca", require: false)
19 |
20 | group :development do
21 | gem("byebug", require: false)
22 | gem("minitest-focus", require: false)
23 | end
24 |
--------------------------------------------------------------------------------
/lib/packwerk/dependency_checker.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | class DependencyChecker
6 | extend T::Sig
7 | include Checker
8 |
9 | sig { override.returns(ViolationType) }
10 | def violation_type
11 | ViolationType::Dependency
12 | end
13 |
14 | sig do
15 | override
16 | .params(reference: Packwerk::Reference)
17 | .returns(T::Boolean)
18 | end
19 | def invalid_reference?(reference)
20 | return false unless reference.source_package
21 | return false unless reference.source_package.enforce_dependencies?
22 | return false if reference.source_package.dependency?(reference.constant.package)
23 | true
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/dev.yml:
--------------------------------------------------------------------------------
1 | name: packwerk
2 |
3 | type: ruby
4 |
5 | up:
6 | - ruby: 3.0.0
7 | - bundler
8 |
9 | commands:
10 | test:
11 | run: |
12 | if [[ "$*" =~ ":"[0-9]+ ]];
13 | then
14 | # run test by its line number
15 | bin/m "$@"
16 | elif [[ "$#" -eq 1 && -f "$1" ]];
17 | then
18 | # run all tests in given file(s)
19 | bin/rake test TEST="$@"
20 | else
21 | # run all tests
22 | bin/rake test
23 | fi
24 | style: "bin/rubocop -D --auto-correct"
25 | typecheck:
26 | desc: "run Sorbet typechecking"
27 | run: "bin/srb tc"
28 | aliases: ['tc']
29 | subcommands:
30 | update:
31 | desc: "update RBIs for gems"
32 | run: "bin/tapioca sync -c 'dev typecheck update'"
33 |
--------------------------------------------------------------------------------
/lib/packwerk/generators/templates/packwerk.yml.erb:
--------------------------------------------------------------------------------
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,vendor}/**/*"
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"
24 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | ENV["RAILS_ENV"] = "test"
5 |
6 | $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
7 |
8 | ROOT = Pathname.new(File.expand_path(Dir.pwd))
9 |
10 | require "packwerk"
11 |
12 | require "minitest/autorun"
13 | require "minitest/focus"
14 | require "mocha/minitest"
15 | require "support/application_fixture_helper"
16 | require "support/factory_helper"
17 | require "support/rails_application_fixture_helper"
18 | require "support/rails_paths"
19 | require "support/test_macro"
20 | require "support/test_assertions"
21 | require "support/yaml_file"
22 |
23 | Minitest::Test.extend(TestMacro)
24 | Minitest::Test.include(TestAssertions)
25 |
26 | Mocha.configure do |c|
27 | c.stubbing_non_existent_method = :prevent
28 | end
29 |
--------------------------------------------------------------------------------
/bin/m:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'm' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "pathname"
12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13 | Pathname.new(__FILE__).realpath)
14 |
15 | bundle_binstub = File.expand_path("../bundle", __FILE__)
16 |
17 | if File.file?(bundle_binstub)
18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19 | load(bundle_binstub)
20 | else
21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23 | end
24 | end
25 |
26 | require "rubygems"
27 | require "bundler/setup"
28 |
29 | load Gem.bin_path("m", "m")
30 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/constant_resolver@0.1.5.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `constant_resolver` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | class ConstantResolver
8 | def initialize(root_path:, load_paths:, inflector: T.unsafe(nil)); end
9 |
10 | def config; end
11 | def file_map; end
12 | def resolve(const_name, current_namespace_path: T.unsafe(nil)); end
13 |
14 | private
15 |
16 | def resolve_constant(const_name, current_namespace_path, original_name: T.unsafe(nil)); end
17 | def resolve_traversing_namespace_path(const_name, current_namespace_path); end
18 | end
19 |
20 | class ConstantResolver::ConstantContext < ::Struct
21 | end
22 |
23 | class ConstantResolver::Error < ::StandardError
24 | end
25 |
26 | ConstantResolver::VERSION = T.let(T.unsafe(nil), String)
27 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'rake' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "pathname"
12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13 | Pathname.new(__FILE__).realpath)
14 |
15 | bundle_binstub = File.expand_path("../bundle", __FILE__)
16 |
17 | if File.file?(bundle_binstub)
18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19 | load(bundle_binstub)
20 | else
21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23 | end
24 | end
25 |
26 | require "rubygems"
27 | require "bundler/setup"
28 |
29 | load Gem.bin_path("rake", "rake")
30 |
--------------------------------------------------------------------------------
/bin/srb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'srb' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "pathname"
12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13 | Pathname.new(__FILE__).realpath)
14 |
15 | bundle_binstub = File.expand_path("../bundle", __FILE__)
16 |
17 | if File.file?(bundle_binstub)
18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19 | load(bundle_binstub)
20 | else
21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23 | end
24 | end
25 |
26 | require "rubygems"
27 | require "bundler/setup"
28 |
29 | load Gem.bin_path("sorbet", "srb")
30 |
--------------------------------------------------------------------------------
/bin/rubocop:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'rubocop' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "pathname"
12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13 | Pathname.new(__FILE__).realpath)
14 |
15 | bundle_binstub = File.expand_path("../bundle", __FILE__)
16 |
17 | if File.file?(bundle_binstub)
18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19 | load(bundle_binstub)
20 | else
21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23 | end
24 | end
25 |
26 | require "rubygems"
27 | require "bundler/setup"
28 |
29 | load Gem.bin_path("rubocop", "rubocop")
30 |
--------------------------------------------------------------------------------
/bin/tapioca:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'tapioca' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "pathname"
12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13 | Pathname.new(__FILE__).realpath)
14 |
15 | bundle_binstub = File.expand_path("../bundle", __FILE__)
16 |
17 | if File.file?(bundle_binstub)
18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19 | load(bundle_binstub)
20 | else
21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23 | end
24 | end
25 |
26 | require "rubygems"
27 | require "bundler/setup"
28 |
29 | load Gem.bin_path("tapioca", "tapioca")
30 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## What are you trying to accomplish?
2 |
3 |
4 | ## What approach did you choose and why?
5 |
6 |
7 | ## What should reviewers focus on?
8 |
9 |
10 | ## Type of Change
11 |
12 | - [ ] Bugfix
13 | - [ ] New feature
14 | - [ ] Non-breaking change (a change that doesn't alter functionality - i.e., code refactor, configs, etc.)
15 |
16 | ### Additional Release Notes
17 |
18 | - [ ] Breaking change (fix or feature that would cause existing functionality to change)
19 |
20 | Include any notes here to include in the release description. For example, if you selected "breaking change" above, leave notes on how users can transition to this version.
21 |
22 | If no additional notes are necessary, delete this section or leave it unchanged.
23 |
24 | ## Checklist
25 |
26 | - [ ] I have updated the documentation accordingly.
27 | - [ ] I have added tests to cover my changes.
28 | - [ ] It is safe to rollback this change.
29 |
--------------------------------------------------------------------------------
/test/support/yaml_file.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | class YamlFile
5 | def initialize(path)
6 | @path = path
7 | end
8 |
9 | def merge(hash)
10 | merged_data = recursive_merge(read_or_create, hash)
11 | write(merged_data)
12 | end
13 |
14 | private
15 |
16 | attr_reader :path
17 |
18 | def read_or_create
19 | FileUtils.mkpath(File.dirname(path))
20 | FileUtils.touch(path)
21 | YAML.load_file(path) || {}
22 | end
23 |
24 | def write(data)
25 | File.open(path, "w") { |f| YAML.dump(data, f) }
26 | end
27 |
28 | def recursive_merge(hash, other_hash)
29 | hash.merge(other_hash) do |_, old_value, new_value|
30 | if old_value.is_a?(Hash) && new_value.is_a?(Hash)
31 | recursive_merge(old_value, new_value)
32 | elsif old_value.is_a?(Array)
33 | old_value + Array(new_value)
34 | else
35 | new_value
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_gem:
2 | rubocop-shopify: rubocop.yml
3 |
4 | require:
5 | - rubocop-performance
6 | - rubocop-sorbet
7 |
8 | AllCops:
9 | TargetRubyVersion: 2.4
10 | UseCache: true
11 | CacheRootDirectory: tmp
12 | Exclude:
13 | - 'test/fixtures/**/*'
14 |
15 | Lint/UnusedMethodArgument:
16 | AllowUnusedKeywordArguments: true
17 |
18 | Style/MethodCallWithArgsParentheses:
19 | Enabled: true
20 | IgnoreMacros: true
21 | IgnoredMethods:
22 | - require
23 | - require_relative
24 | - require_dependency
25 | - yield
26 | - raise
27 | Exclude:
28 | - Gemfile
29 |
30 | Style/StringLiterals:
31 | EnforcedStyle: double_quotes
32 |
33 | Sorbet/ConstantsFromStrings:
34 | Enabled: true
35 |
36 | Sorbet/ForbidIncludeConstLiteral:
37 | Enabled: true
38 |
39 | Sorbet/ParametersOrderingInSignature:
40 | Enabled: true
41 |
42 | Sorbet/KeywordArgumentOrdering:
43 | Enabled: true
44 |
45 | Sorbet/ValidSigil:
46 | Enabled: true
47 |
--------------------------------------------------------------------------------
/lib/packwerk/offense.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "parser/source/map"
5 |
6 | module Packwerk
7 | class Offense
8 | extend T::Sig
9 | extend T::Helpers
10 |
11 | attr_reader :location, :file, :message
12 |
13 | sig do
14 | params(file: String, message: String, location: T.nilable(Node::Location))
15 | .void
16 | end
17 | def initialize(file:, message:, location: nil)
18 | @location = location
19 | @file = file
20 | @message = message
21 | end
22 |
23 | sig { params(style: OutputStyle).returns(String) }
24 | def to_s(style = OutputStyles::Plain.new)
25 | if location
26 | <<~EOS
27 | #{style.filename}#{file}#{style.reset}:#{location.line}:#{location.column}
28 | #{@message}
29 | EOS
30 | else
31 | <<~EOS
32 | #{style.filename}#{file}#{style.reset}
33 | #{@message}
34 | EOS
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/packwerk/generators/root_package.rb:
--------------------------------------------------------------------------------
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
38 |
--------------------------------------------------------------------------------
/lib/packwerk/parsers/factory.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "singleton"
5 |
6 | module Packwerk
7 | module Parsers
8 | class Factory
9 | include Singleton
10 |
11 | RUBY_REGEX = %r{
12 | # Although not important for regex, these are ordered from most likely to match to least likely.
13 | \.(rb|rake|builder|gemspec|ru)\Z
14 | |
15 | (Gemfile|Rakefile)\Z
16 | }x
17 | private_constant :RUBY_REGEX
18 |
19 | ERB_REGEX = /\.erb\Z/
20 | private_constant :ERB_REGEX
21 |
22 | def for_path(path)
23 | case path
24 | when RUBY_REGEX
25 | @ruby_parser ||= Ruby.new
26 | when ERB_REGEX
27 | @erb_parser ||= erb_parser_class.new
28 | end
29 | end
30 |
31 | def erb_parser_class
32 | @erb_parser_class ||= Erb
33 | end
34 |
35 | def erb_parser_class=(klass)
36 | @erb_parser_class = klass
37 | @erb_parser = nil
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/support/rails_paths.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | class RailsPaths
5 | class << self
6 | def autoload(paths)
7 | Rails.application.config.autoload_paths = paths
8 | end
9 |
10 | def eager_load(paths)
11 | Rails.application.config.eager_load_paths = paths
12 | end
13 |
14 | def autoload_once(paths)
15 | Rails.application.config.autoload_once_paths = paths
16 | end
17 |
18 | def root(path)
19 | Rails.application.config.root = path
20 | end
21 | end
22 |
23 | def cache
24 | @autoload_paths = Rails.application.config.autoload_paths
25 | @eager_load_paths = Rails.application.config.eager_load_paths
26 | @autoload_once_paths = Rails.application.config.autoload_once_paths
27 | @root_path = Rails.application.config.root
28 | end
29 |
30 | def restore
31 | self.class.autoload(@autoload_paths)
32 | self.class.eager_load(@eager_load_paths)
33 | self.class.autoload_once(@autoload_once_paths)
34 | self.class.root(@root_path)
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2020-present, Shopify Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/test/fixtures/formats/erb/valid.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= instrumentation_header %>
5 |
6 |
7 |
8 |
9 | <%= tag :meta, name: "referrer", content: "never" %>
10 |
11 | <%= @title || "My Test Site" %>
12 |
13 | <%= stylesheet_link_tag MyNamespace::MY_CONSTANT, integrity: true, crossorigin: "anonymous" %>
14 | <%= javascript_include_tag "jquery", integrity: true, crossorigin: "anonymous" %>
15 |
16 | <%= csrf_meta_tag %>
17 |
18 | <%= yield :header %>
19 |
20 | <%# here's the body %>
21 |
22 |
23 | <% @things.each do |thing| %>
24 | - <%= thing %>
25 | <% end %>
26 |
27 |
28 |
29 |
30 | <%= yield :body %>
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/lib/packwerk/node_processor_factory.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | class NodeProcessorFactory < T::Struct
6 | extend T::Sig
7 |
8 | const :root_path, String
9 | const :context_provider, Packwerk::ConstantDiscovery
10 | const :constant_name_inspectors, T::Array[ConstantNameInspector]
11 | const :checkers, T::Array[Checker]
12 |
13 | sig { params(filename: String, node: AST::Node).returns(NodeProcessor) }
14 | def for(filename:, node:)
15 | ::Packwerk::NodeProcessor.new(
16 | reference_extractor: reference_extractor(node: node),
17 | filename: filename,
18 | checkers: checkers,
19 | )
20 | end
21 |
22 | private
23 |
24 | sig { params(node: AST::Node).returns(::Packwerk::ReferenceExtractor) }
25 | def reference_extractor(node:)
26 | ::Packwerk::ReferenceExtractor.new(
27 | context_provider: context_provider,
28 | constant_name_inspectors: constant_name_inspectors,
29 | root_node: node,
30 | root_path: root_path,
31 | )
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/packwerk/inflections/custom.rb:
--------------------------------------------------------------------------------
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
34 |
--------------------------------------------------------------------------------
/lib/packwerk/generators/templates/package.yml:
--------------------------------------------------------------------------------
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 | # By default the public path will be app/public/, however this may not suit all applications' architecture so
15 | # this allows you to modify what your package's public path is.
16 | # public_path: app/public/
17 |
18 | # A list of this package's dependencies
19 | # Note that packages in this list require their own `package.yml` file
20 | # dependencies:
21 | # - "packages/billing"
22 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/erubi@1.10.0.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `erubi` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | module Erubi
8 | class << self
9 | def h(value); end
10 | end
11 | end
12 |
13 | class Erubi::Engine
14 | def initialize(input, properties = T.unsafe(nil)); end
15 |
16 | def bufvar; end
17 | def filename; end
18 | def src; end
19 |
20 | private
21 |
22 | def add_code(code); end
23 | def add_expression(indicator, code); end
24 | def add_expression_result(code); end
25 | def add_expression_result_escaped(code); end
26 | def add_postamble(postamble); end
27 | def add_text(text); end
28 | def handle(indicator, code, tailch, rspace, lspace); end
29 | end
30 |
31 | Erubi::MATCH_METHOD = T.let(T.unsafe(nil), Symbol)
32 |
33 | Erubi::RANGE_ALL = T.let(T.unsafe(nil), Range)
34 |
35 | Erubi::RANGE_FIRST = T.let(T.unsafe(nil), Integer)
36 |
37 | Erubi::RANGE_LAST = T.let(T.unsafe(nil), Integer)
38 |
39 | Erubi::TEXT_END = T.let(T.unsafe(nil), String)
40 |
41 | Erubi::VERSION = T.let(T.unsafe(nil), String)
42 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/colorize@0.8.1.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `colorize` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | module Colorize
8 | end
9 |
10 | module Colorize::ClassMethods
11 | def color_codes; end
12 | def color_matrix(_ = T.unsafe(nil)); end
13 | def color_methods; end
14 | def color_samples; end
15 | def colors; end
16 | def disable_colorization(value = T.unsafe(nil)); end
17 | def disable_colorization=(value); end
18 | def mode_codes; end
19 | def modes; end
20 | def modes_methods; end
21 | end
22 |
23 | module Colorize::InstanceMethods
24 | def colorize(params); end
25 | def colorized?; end
26 | def uncolorize; end
27 |
28 | private
29 |
30 | def background_color(color); end
31 | def color(color); end
32 | def color_from_symbol(match, symbol); end
33 | def colors_from_hash(match, hash); end
34 | def colors_from_params(match, params); end
35 | def defaults_colors(match); end
36 | def mode(mode); end
37 | def require_windows_libs; end
38 | def scan_for_colors; end
39 | def split_colors(match); end
40 | end
41 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/html_tokenizer@0.0.7.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `html_tokenizer` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | module HtmlTokenizer
8 | end
9 |
10 | class HtmlTokenizer::Parser
11 | def initialize; end
12 |
13 | def append_placeholder(_); end
14 | def attribute_name; end
15 | def attribute_quoted?; end
16 | def attribute_value; end
17 | def cdata_text; end
18 | def closing_tag?; end
19 | def column_number; end
20 | def comment_text; end
21 | def context; end
22 | def document; end
23 | def document_length; end
24 | def errors; end
25 | def errors_count; end
26 | def line_number; end
27 | def parse(_); end
28 | def quote_character; end
29 | def rawtext_text; end
30 | def self_closing_tag?; end
31 | def tag_name; end
32 | end
33 |
34 | class HtmlTokenizer::ParserError < ::RuntimeError
35 | def initialize(message, position, line, column); end
36 |
37 | def column; end
38 | def line; end
39 | def position; end
40 | end
41 |
42 | class HtmlTokenizer::Tokenizer
43 | def initialize; end
44 |
45 | def tokenize(_); end
46 | end
47 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Issue reporting
4 | * Check to make sure the same issue has not already been reported or fixed
5 | * Open an issue with a descriptive title and summary
6 | * Be clear and concise and provide as many details as possible (e.g. Ruby version, Packwerk version, etc.)
7 | * Include relevant code, where necessary
8 |
9 | ## Pull requests
10 | * Read and understand our [Code of Conduct](https://github.com/Shopify/packwerk/blob/main/CODE_OF_CONDUCT.md)
11 | * Make sure tests are added for any changes to the code
12 | * [Squash related commits together](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
13 | * If the change includes any new keys to `packwerk.yml`, make sure that the [application validator](https://github.com/Shopify/packwerk/blob/1c711748b4a28b65220e2cefba764ffd8eb1a101/lib/packwerk/application_validator.rb#L116) is aligned with that change
14 | * Open a pull request once the change is ready to be reviewed
15 | * Be descriptive about the problem and reason about the proposed solution
16 | * Include release notes describing the potential impact of the change in the pull request
17 | * Make sure there has been at least one approval from Shopify before merging
18 |
--------------------------------------------------------------------------------
/test/unit/generators/root_package_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | module Generators
8 | class RootPackageTest < Minitest::Test
9 | setup do
10 | @string_io = StringIO.new
11 | @temp_dir = Dir.mktmpdir
12 | @generated_file_path = File.join(@temp_dir, "package.yml")
13 | end
14 |
15 | teardown do
16 | FileUtils.remove_entry(@temp_dir)
17 | end
18 |
19 | test ".generate creates a package.yml file" do
20 | success = Packwerk::Generators::RootPackage.generate(root: @temp_dir, out: @string_io)
21 | assert(File.exist?(@generated_file_path))
22 | assert success
23 | assert_includes @string_io.string, "root package generated"
24 | end
25 |
26 | test ".generate does not create a package.yml file if package.yml already exists" do
27 | File.open(File.join(@temp_dir, "package.yml"), "w") do |_f|
28 | success = Packwerk::Generators::RootPackage.generate(root: @temp_dir, out: @string_io)
29 | assert success
30 | assert_includes @string_io.string, "Root package already exists"
31 | end
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/packwerk/inflector.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "active_support/inflector"
5 |
6 | module Packwerk
7 | class Inflector
8 | class << self
9 | extend T::Sig
10 |
11 | def default
12 | @default ||= new(custom_inflector: Inflections::Custom.new)
13 | end
14 |
15 | sig { params(inflections_file: String).returns(::Packwerk::Inflector) }
16 | def from_file(inflections_file)
17 | new(custom_inflector: Inflections::Custom.new(inflections_file))
18 | end
19 | end
20 |
21 | extend T::Sig
22 | include ::ActiveSupport::Inflector # For #camelize, #classify, #pluralize, #singularize
23 |
24 | sig do
25 | params(
26 | custom_inflector: Inflections::Custom
27 | ).void
28 | end
29 | def initialize(custom_inflector:)
30 | @inflections = ::ActiveSupport::Inflector::Inflections.new
31 |
32 | Inflections::Default.apply_to(@inflections)
33 | custom_inflector.apply_to(@inflections)
34 | end
35 |
36 | def pluralize(word, count = nil)
37 | if count == 1
38 | singularize(word)
39 | else
40 | super(word)
41 | end
42 | end
43 |
44 | def inflections(_ = nil)
45 | @inflections
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/packwerk/generators/inflections_file.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | module Generators
6 | class InflectionsFile
7 | extend T::Sig
8 |
9 | class << self
10 | def generate(root:, out:)
11 | new(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 | ruby_inflection_file_exist = Dir.glob("#{@root}/**/inflections.rb").any?
23 | yaml_inflection_file_exist = Dir.glob("#{@root}/**/inflections.yml").any?
24 |
25 | if !ruby_inflection_file_exist || yaml_inflection_file_exist
26 | return true
27 | end
28 |
29 | @out.puts("📦 Generating `inflections.yml` file...")
30 |
31 | destination_file_path = File.join(@root, "config")
32 | FileUtils.mkdir_p(destination_file_path)
33 |
34 | source_file_path = File.join(__dir__, "/templates/inflections.yml")
35 | FileUtils.cp(source_file_path, destination_file_path)
36 |
37 | @out.puts("✅ `inflections.yml` generated in #{destination_file_path}")
38 |
39 | true
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/packwerk/file_processor.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "ast/node"
5 |
6 | module Packwerk
7 | class FileProcessor
8 | class UnknownFileTypeResult < Offense
9 | def initialize(file:)
10 | super(file: file, message: "unknown file type")
11 | end
12 | end
13 |
14 | def initialize(node_processor_factory:, parser_factory: nil)
15 | @node_processor_factory = node_processor_factory
16 | @parser_factory = parser_factory || Packwerk::Parsers::Factory.instance
17 | end
18 |
19 | def call(file_path)
20 | parser = @parser_factory.for_path(file_path)
21 | return [UnknownFileTypeResult.new(file: file_path)] if parser.nil?
22 |
23 | node = File.open(file_path, "r", external_encoding: Encoding::UTF_8) do |file|
24 | begin
25 | parser.call(io: file, file_path: file_path)
26 | rescue Parsers::ParseError => e
27 | return [e.result]
28 | end
29 | end
30 |
31 | result = []
32 | if node
33 | node_processor = @node_processor_factory.for(filename: file_path, node: node)
34 | node_visitor = Packwerk::NodeVisitor.new(node_processor: node_processor)
35 |
36 | node_visitor.visit(node, ancestors: [], result: result)
37 | end
38 | result
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/packwerk/parsers/ruby.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "parser"
5 | require "parser/current"
6 |
7 | module Packwerk
8 | module Parsers
9 | class Ruby
10 | class RaiseExceptionsParser < Parser::CurrentRuby
11 | def initialize(builder)
12 | super(builder)
13 | super.diagnostics.all_errors_are_fatal = true
14 | end
15 | end
16 |
17 | class TolerateInvalidUtf8Builder < Parser::Builders::Default
18 | def string_value(token)
19 | value(token)
20 | end
21 | end
22 |
23 | def initialize(parser_class: RaiseExceptionsParser)
24 | @builder = TolerateInvalidUtf8Builder.new
25 | @parser_class = parser_class
26 | end
27 |
28 | def call(io:, file_path: "")
29 | buffer = Parser::Source::Buffer.new(file_path)
30 | buffer.source = io.read
31 | parser = @parser_class.new(@builder)
32 | parser.parse(buffer)
33 | rescue EncodingError => e
34 | result = ParseResult.new(file: file_path, message: e.message)
35 | raise Parsers::ParseError, result
36 | rescue Parser::SyntaxError => e
37 | result = ParseResult.new(file: file_path, message: "Syntax error: #{e}")
38 | raise Parsers::ParseError, result
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/test/unit/formatters/offenses_formatter_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | module Formatters
8 | class OffensesFormatterTest < Minitest::Test
9 | setup do
10 | @offenses_formatter = OffensesFormatter.new
11 | end
12 |
13 | test "#show_offenses prints No offenses detected when there are no offenses" do
14 | assert_match "No offenses detected", @offenses_formatter.show_offenses([])
15 | end
16 |
17 | test "#show_offenses prints the amount of files when there are offenses" do
18 | offense = Offense.new(file: "first_file.rb", message: "an offense")
19 | another_offense = Offense.new(file: "second_file.rb", message: "another offense")
20 | assert_match "2 offenses detected", @offenses_formatter.show_offenses([offense, another_offense])
21 | end
22 |
23 | test "#show_offenses prints the files with offenses" do
24 | offense = Offense.new(file: "first_file.rb", message: "an offense")
25 | another_offense = Offense.new(file: "second_file.rb", message: "another offense")
26 | assert_match offense.to_s, @offenses_formatter.show_offenses([offense, another_offense])
27 | assert_match another_offense.to_s, @offenses_formatter.show_offenses([offense, another_offense])
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/ast@2.4.2.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `ast` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | module AST
8 | end
9 |
10 | class AST::Node
11 | def initialize(type, children = T.unsafe(nil), properties = T.unsafe(nil)); end
12 |
13 | def +(array); end
14 | def <<(element); end
15 | def ==(other); end
16 | def append(element); end
17 | def children; end
18 | def clone; end
19 | def concat(array); end
20 | def deconstruct; end
21 | def dup; end
22 | def eql?(other); end
23 | def hash; end
24 | def inspect(indent = T.unsafe(nil)); end
25 | def to_a; end
26 | def to_ast; end
27 | def to_s(indent = T.unsafe(nil)); end
28 | def to_sexp(indent = T.unsafe(nil)); end
29 | def to_sexp_array; end
30 | def type; end
31 | def updated(type = T.unsafe(nil), children = T.unsafe(nil), properties = T.unsafe(nil)); end
32 |
33 | protected
34 |
35 | def assign_properties(properties); end
36 | def fancy_type; end
37 |
38 | private
39 |
40 | def original_dup; end
41 | end
42 |
43 | class AST::Processor
44 | include(::AST::Processor::Mixin)
45 | end
46 |
47 | module AST::Processor::Mixin
48 | def handler_missing(node); end
49 | def process(node); end
50 | def process_all(nodes); end
51 | end
52 |
53 | module AST::Sexp
54 | def s(type, *children); end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/packwerk/node_processor.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | class NodeProcessor
6 | extend T::Sig
7 |
8 | sig do
9 | params(
10 | reference_extractor: ReferenceExtractor,
11 | filename: String,
12 | checkers: T::Array[Checker]
13 | ).void
14 | end
15 | def initialize(reference_extractor:, filename:, checkers:)
16 | @reference_extractor = reference_extractor
17 | @filename = filename
18 | @checkers = checkers
19 | end
20 |
21 | sig { params(node: Parser::AST::Node, ancestors: T::Array[Parser::AST::Node]).returns(T::Array[Offense]) }
22 | def call(node, ancestors)
23 | return [] unless Node.method_call?(node) || Node.constant?(node)
24 | reference = @reference_extractor.reference_from_node(node, ancestors: ancestors, file_path: @filename)
25 | check_reference(reference, node)
26 | end
27 |
28 | private
29 |
30 | def check_reference(reference, node)
31 | return [] unless reference
32 | @checkers.each_with_object([]) do |checker, violations|
33 | next unless checker.invalid_reference?(reference)
34 | offense = Packwerk::ReferenceOffense.new(
35 | location: Node.location(node),
36 | reference: reference,
37 | violation_type: checker.violation_type
38 | )
39 | violations << offense
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/test/unit/dependency_checker_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | class DependencyCheckerTest < Minitest::Test
8 | include FactoryHelper
9 |
10 | test "recognizes simple cross package reference" do
11 | source_package = Package.new(name: "components/sales", config: { "enforce_dependencies" => true })
12 | checker = dependency_checker
13 | reference = build_reference(source_package: source_package)
14 |
15 | assert checker.invalid_reference?(reference)
16 | end
17 |
18 | test "ignores violations when enforcement is disabled in that package" do
19 | source_package = Package.new(name: "components/sales", config: { "enforce_dependencies" => false })
20 | checker = dependency_checker
21 | reference = build_reference(source_package: source_package)
22 |
23 | refute checker.invalid_reference?(reference)
24 | end
25 |
26 | test "allows reference to constants of a declared dependency" do
27 | source_package = Package.new(
28 | name: "components/sales",
29 | config: { "enforce_dependencies" => true, "dependencies" => ["components/destination"] }
30 | )
31 | checker = dependency_checker
32 | reference = build_reference(source_package: source_package)
33 |
34 | refute checker.invalid_reference?(reference)
35 | end
36 |
37 | private
38 |
39 | def dependency_checker
40 | DependencyChecker.new
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/test/unit/graph_test.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | class GraphTest < Minitest::Test
8 | test "#acyclic? returns true for a directed acyclic graph" do
9 | graph = Graph.new([1, 2], [1, 3], [2, 4], [3, 4])
10 |
11 | assert_predicate graph, :acyclic?
12 | end
13 |
14 | test "#acyclic? returns false for a cyclic graph" do
15 | graph = Graph.new([1, 2], [2, 3], [3, 1])
16 |
17 | refute_predicate graph, :acyclic?
18 | end
19 |
20 | test "#cycles returns all cycles in a graph" do
21 | #
22 | # 1 -> 2 <-> 3
23 | # \
24 | # \
25 | # -> 4 --> 5
26 | # ^ \
27 | # | |
28 | # +- 6 <-
29 | #
30 | graph = Graph.new([1, 2], [2, 3], [3, 2], [1, 4], [4, 5], [5, 6], [6, 4])
31 |
32 | assert_equal [[2, 3], [4, 5, 6]], graph.cycles.sort
33 | end
34 |
35 | test "#cycles returns overlapping cycles in a graph" do
36 | graph = Graph.new([1, 2], [2, 3], [1, 4], [4, 3], [3, 1])
37 |
38 | assert_equal [[1, 2, 3], [1, 4, 3]], graph.cycles.sort
39 | end
40 |
41 | test "#cycles returns cycles in a graph with disjoint subgraphs" do
42 | graph = Graph.new(
43 | [1, 2], [2, 3], [3, 1],
44 | [4, 5], [4, 6], [5, 7], [6, 7],
45 | [8, 9], [9, 8], [8, 10], [10, 11], [8, 11],
46 | )
47 |
48 | assert_equal [[1, 2, 3], [8, 9]], graph.cycles.sort
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/packwerk/formatters/progress_formatter.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "benchmark"
5 |
6 | module Packwerk
7 | module Formatters
8 | class ProgressFormatter
9 | extend T::Sig
10 |
11 | sig { params(out: T.any(StringIO, IO), style: OutputStyle).void }
12 | def initialize(out, style: OutputStyles::Plain.new)
13 | @out = out
14 | @style = style
15 | end
16 |
17 | def started(target_files)
18 | files_size = target_files.size
19 | files_string = Inflector.default.pluralize("file", files_size)
20 | @out.puts("📦 Packwerk is inspecting #{files_size} #{files_string}")
21 | end
22 |
23 | def started_validation
24 | @out.puts("📦 Packwerk is running validation...")
25 |
26 | execution_time = Benchmark.realtime { yield }
27 | finished(execution_time)
28 |
29 | @out.puts("✅ Packages are valid. Use `packwerk check` to run static checks.")
30 | end
31 |
32 | def mark_as_inspected
33 | @out.print(".")
34 | end
35 |
36 | def mark_as_failed
37 | @out.print("#{@style.error}E#{@style.reset}")
38 | end
39 |
40 | def finished(execution_time)
41 | @out.puts
42 | @out.puts("📦 Finished in #{execution_time.round(2)} seconds")
43 | end
44 |
45 | def interrupted
46 | @out.puts
47 | @out.puts("Manually interrupted. Violations caught so far are listed below:")
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/packwerk/privacy_checker.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | class PrivacyChecker
6 | extend T::Sig
7 | include Checker
8 |
9 | sig { override.returns(Packwerk::ViolationType) }
10 | def violation_type
11 | ViolationType::Privacy
12 | end
13 |
14 | sig do
15 | override
16 | .params(reference: Packwerk::Reference)
17 | .returns(T::Boolean)
18 | end
19 | def invalid_reference?(reference)
20 | return false if reference.constant.public?
21 |
22 | privacy_option = reference.constant.package.enforce_privacy
23 | return false if enforcement_disabled?(privacy_option)
24 |
25 | return false unless privacy_option == true ||
26 | explicitly_private_constant?(reference.constant, explicitly_private_constants: privacy_option)
27 |
28 | true
29 | end
30 |
31 | private
32 |
33 | sig do
34 | params(
35 | constant: ConstantDiscovery::ConstantContext,
36 | explicitly_private_constants: T::Array[String]
37 | ).returns(T::Boolean)
38 | end
39 | def explicitly_private_constant?(constant, explicitly_private_constants:)
40 | explicitly_private_constants.include?(constant.name) ||
41 | # nested constants
42 | explicitly_private_constants.any? { |epc| constant.name.start_with?(epc + "::") }
43 | end
44 |
45 | sig do
46 | params(privacy_option: T.nilable(T.any(T::Boolean, T::Array[String])))
47 | .returns(T::Boolean)
48 | end
49 | def enforcement_disabled?(privacy_option)
50 | [false, nil].include?(privacy_option)
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | gemfile:
11 | - Gemfile
12 | - gemfiles/Gemfile-rails-6-0
13 | ruby:
14 | - 2.6
15 | - 2.7
16 | - 3.0
17 | exclude:
18 | - ruby: 2.6
19 | gemfile: Gemfile
20 | env:
21 | BUNDLE_GEMFILE: ${{ matrix.gemfile }}
22 | name: "Tests: Ruby ${{ matrix.ruby }} ${{ matrix.gemfile }}"
23 | steps:
24 | - uses: actions/checkout@v1
25 | - name: Set up Ruby ${{ matrix.ruby }}
26 | uses: actions/setup-ruby@v1
27 | with:
28 | ruby-version: ${{ matrix.ruby }}
29 | - name: Run tests
30 | run: |
31 | gem install bundler
32 | bundle install --jobs 4 --retry 3
33 | bin/rake
34 | lint:
35 | runs-on: ubuntu-latest
36 | steps:
37 | - uses: actions/checkout@v1
38 | - name: Set up Ruby
39 | uses: actions/setup-ruby@v1
40 | with:
41 | ruby-version: 3.0.x
42 | - name: Run style checks
43 | run: |
44 | gem install bundler
45 | bundle install --jobs 4 --retry 3
46 | bin/rubocop
47 | static-type-checking:
48 | runs-on: ubuntu-latest
49 | steps:
50 | - uses: actions/checkout@v1
51 | - name: Set up Ruby
52 | uses: actions/setup-ruby@v1
53 | with:
54 | ruby-version: 3.0.x
55 | - name: Run static type checks
56 | run: |
57 | gem install bundler
58 | bundle install --jobs 4 --retry 3
59 | bin/srb tc
60 |
--------------------------------------------------------------------------------
/test/support/rails_application_fixture_helper.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "rails_test_helper"
5 |
6 | module RailsApplicationFixtureHelper
7 | include ApplicationFixtureHelper
8 |
9 | def setup_application_fixture
10 | super()
11 | cache_rails_paths
12 | end
13 |
14 | def teardown_application_fixture
15 | restore_rails_paths
16 | super()
17 | end
18 |
19 | def use_template(template)
20 | super(template)
21 |
22 | case template
23 | when :minimal
24 | set_load_paths_for_minimal_template
25 | when :skeleton
26 | set_load_paths_for_skeleton_template
27 | else
28 | raise "Unknown fixture template #{template}"
29 | end
30 |
31 | RailsPaths.root(app_dir)
32 | end
33 |
34 | private
35 |
36 | def set_load_paths_for_minimal_template
37 | RailsPaths.autoload(to_app_paths("/components/sales/app/models"))
38 | RailsPaths.eager_load([])
39 | RailsPaths.autoload_once([])
40 | end
41 |
42 | def set_load_paths_for_skeleton_template
43 | RailsPaths.autoload(to_app_paths("/components/sales/app/models"))
44 | RailsPaths.eager_load(to_app_paths("components/platform/app/models"))
45 | RailsPaths.autoload_once(
46 | to_app_paths(
47 | "components/timeline/app/models",
48 | "components/timeline/app/models/concerns",
49 | "vendor/cache/gems/example/models",
50 | )
51 | )
52 | end
53 |
54 | def cache_rails_paths
55 | raise "cache_rails_paths may only be called once per test" if defined? @rails_paths
56 | @rails_paths = RailsPaths.new
57 | @rails_paths.cache
58 | end
59 |
60 | def restore_rails_paths
61 | @rails_paths.restore
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/packwerk/formatters/offenses_formatter.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | module Formatters
6 | class OffensesFormatter
7 | include Packwerk::OffensesFormatter
8 |
9 | extend T::Sig
10 |
11 | sig { params(style: OutputStyle).void }
12 | def initialize(style: OutputStyles::Plain.new)
13 | @style = style
14 | end
15 |
16 | sig { override.params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
17 | def show_offenses(offenses)
18 | return "No offenses detected" if offenses.empty?
19 |
20 | <<~EOS
21 | #{offenses_list(offenses)}
22 | #{offenses_summary(offenses)}
23 | EOS
24 | end
25 |
26 | sig { override.params(offense_collection: Packwerk::OffenseCollection).returns(String) }
27 | def show_stale_violations(offense_collection)
28 | if offense_collection.stale_violations?
29 | "There were stale violations found, please run `packwerk update-deprecations`"
30 | else
31 | "No stale violations detected"
32 | end
33 | end
34 |
35 | private
36 |
37 | sig { params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
38 | def offenses_list(offenses)
39 | offenses
40 | .compact
41 | .map { |offense| offense.to_s(@style) }
42 | .join("\n")
43 | end
44 |
45 | sig { params(offenses: T::Array[T.nilable(Offense)]).returns(String) }
46 | def offenses_summary(offenses)
47 | offenses_string = Inflector.default.pluralize("offense", offenses.length)
48 | "#{offenses.length} #{offenses_string} detected"
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/test/unit/generators/inflections_file_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | module Generators
8 | class InflectionsFileTest < Minitest::Test
9 | setup do
10 | @string_io = StringIO.new
11 | @temp_dir = Dir.mktmpdir
12 | end
13 |
14 | teardown do
15 | FileUtils.remove_entry(@temp_dir)
16 | end
17 |
18 | test ".generate creates an inflections.yml file only if inflections.rb exists" do
19 | File.open(File.join(@temp_dir, "inflections.rb"), "w") do |_f|
20 | generated_file_path = File.join(@temp_dir, "config", "inflections.yml")
21 | success = Packwerk::Generators::InflectionsFile.generate(root: @temp_dir, out: @string_io)
22 | assert(File.exist?(generated_file_path))
23 | assert success
24 | end
25 | end
26 |
27 | test ".generate does not create an inflections.yml file if inflections.rb doesn't exists" do
28 | generated_file_path = File.join(@temp_dir, "config", "inflections.yml")
29 | success = Packwerk::Generators::InflectionsFile.generate(root: @temp_dir, out: @string_io)
30 | refute(File.exist?(generated_file_path))
31 | assert success
32 | end
33 |
34 | test ".generate does not create an inflections.yml file if inflections.yml already exists" do
35 | File.open(File.join(@temp_dir, "inflections.yml"), "w") do |_f|
36 | generated_file_path = File.join(@temp_dir, "config", "inflections.yml")
37 | success = Packwerk::Generators::InflectionsFile.generate(root: @temp_dir, out: @string_io)
38 | refute(File.exist?(generated_file_path))
39 | assert success
40 | end
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/packwerk/package.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | class Package
6 | include Comparable
7 |
8 | ROOT_PACKAGE_NAME = "."
9 |
10 | attr_reader :name, :dependencies
11 |
12 | def initialize(name:, config:)
13 | @name = name
14 | @config = config || {}
15 | @dependencies = Array(@config["dependencies"]).freeze
16 | end
17 |
18 | def enforce_privacy
19 | @config["enforce_privacy"]
20 | end
21 |
22 | def enforce_dependencies?
23 | @config["enforce_dependencies"] == true
24 | end
25 |
26 | def dependency?(package)
27 | @dependencies.include?(package.name)
28 | end
29 |
30 | def package_path?(path)
31 | return true if root?
32 | path.start_with?(@name)
33 | end
34 |
35 | def public_path
36 | @public_path ||= begin
37 | unprefixed_public_path = user_defined_public_path || "app/public/"
38 |
39 | if root?
40 | unprefixed_public_path
41 | else
42 | File.join(@name, unprefixed_public_path)
43 | end
44 | end
45 | end
46 |
47 | def public_path?(path)
48 | path.start_with?(public_path)
49 | end
50 |
51 | def user_defined_public_path
52 | return unless @config["public_path"]
53 | return @config["public_path"] if @config["public_path"].end_with?("/")
54 |
55 | @config["public_path"] + "/"
56 | end
57 |
58 | def <=>(other)
59 | return nil unless other.is_a?(self.class)
60 | name <=> other.name
61 | end
62 |
63 | def eql?(other)
64 | self == other
65 | end
66 |
67 | def hash
68 | name.hash
69 | end
70 |
71 | def to_s
72 | name
73 | end
74 |
75 | def root?
76 | @name == ROOT_PACKAGE_NAME
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/test/integration/offense_collection_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 | require "rails_test_helper"
6 |
7 | module Packwerk
8 | module Integration
9 | class OffenseCollectionTest < Minitest::Test
10 | include ApplicationFixtureHelper
11 | include FactoryHelper
12 |
13 | setup do
14 | setup_application_fixture
15 | use_template(:blank)
16 | @offense_collection = OffenseCollection.new(app_dir)
17 | end
18 |
19 | teardown do
20 | teardown_application_fixture
21 | end
22 |
23 | test "#add_violation for two instances of the same logical package amalgamates both offenses" do
24 | offense1 = ReferenceOffense.new(
25 | reference: build_reference(
26 | constant_name: "::Foo",
27 | source_package: Package.new(name: ".", config: nil)
28 | ),
29 | violation_type: ViolationType::Dependency
30 | )
31 | offense2 = ReferenceOffense.new(
32 | reference: build_reference(
33 | constant_name: "::Bar",
34 | source_package: Package.new(name: ".", config: nil)
35 | ),
36 | violation_type: ViolationType::Dependency
37 | )
38 | @offense_collection.add_offense(offense1)
39 | @offense_collection.add_offense(offense2)
40 | @offense_collection.dump_deprecated_references_files
41 |
42 | expected = {
43 | "components/destination" => {
44 | "::Bar" => { "violations" => ["dependency"], "files" => ["some/path.rb"] },
45 | "::Foo" => { "violations" => ["dependency"], "files" => ["some/path.rb"] },
46 | },
47 | }
48 | assert_equal expected, YAML.load_file("deprecated_references.yml")
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/packwerk/files_for_processing.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | class FilesForProcessing
6 | class << self
7 | def fetch(paths:, configuration:)
8 | new(paths, configuration).files
9 | end
10 | end
11 |
12 | def initialize(paths, configuration)
13 | @paths = paths
14 | @configuration = configuration
15 | end
16 |
17 | def files
18 | include_files = if custom_files.empty?
19 | configured_included_files
20 | else
21 | custom_files
22 | end
23 |
24 | include_files - configured_excluded_files
25 | end
26 |
27 | private
28 |
29 | def custom_files
30 | @custom_files ||= @paths.flat_map do |path|
31 | path = File.expand_path(path, @configuration.root_path)
32 | if File.file?(path)
33 | path
34 | else
35 | custom_included_files(path)
36 | end
37 | end
38 | end
39 |
40 | def custom_included_files(path)
41 | # Note, assuming include globs are always relative paths
42 | absolute_includes = @configuration.include.map do |glob|
43 | File.expand_path(glob, @configuration.root_path)
44 | end
45 |
46 | Dir.glob([File.join(path, "**", "*")]).select do |file_path|
47 | absolute_includes.any? do |pattern|
48 | File.fnmatch?(pattern, file_path, File::FNM_EXTGLOB)
49 | end
50 | end
51 | end
52 |
53 | def configured_included_files
54 | files_for_globs(@configuration.include)
55 | end
56 |
57 | def configured_excluded_files
58 | files_for_globs(@configuration.exclude)
59 | end
60 |
61 | def files_for_globs(globs)
62 | globs
63 | .flat_map { |glob| Dir[File.expand_path(glob, @configuration.root_path)] }
64 | .uniq
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/test/unit/formatters/progress_formatter_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | module Formatters
8 | class ProgressFormatterTest < Minitest::Test
9 | setup do
10 | @string_io = StringIO.new
11 | @progress_formatter = ProgressFormatter.new(@string_io)
12 | end
13 |
14 | test "#started prints the right file size for multiple files" do
15 | @progress_formatter.started([1, 2, 3, 4, 5])
16 | assert_match "5", @string_io.string
17 | assert_match "files", @string_io.string
18 | end
19 |
20 | test "#started prints the right file size for single files" do
21 | @progress_formatter.started([1])
22 | assert_match "1", @string_io.string
23 | assert_match "file", @string_io.string
24 | end
25 |
26 | test "#started prints the right file size for no files" do
27 | @progress_formatter.started([])
28 | assert_match "0 files", @string_io.string
29 | end
30 |
31 | test "#started_validation yields control to code block" do
32 | @progress_formatter.started_validation do
33 | @string_io.puts("This block has been run")
34 | end
35 |
36 | assert_match "This block has been run", @string_io.string
37 | end
38 |
39 | test "#mark_as_inspected prints a dot" do
40 | @progress_formatter.mark_as_inspected
41 | assert_equal ".", @string_io.string
42 | end
43 |
44 | test "#mark_as_failed prints an E" do
45 | @progress_formatter.mark_as_failed
46 | assert_equal "E", @string_io.string
47 | end
48 |
49 | test "#finished prints the correct time" do
50 | @progress_formatter.finished(1.1234)
51 | assert_match "1.12", @string_io.string
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/mini_mime@1.0.3.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `mini_mime` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | module MiniMime
8 | class << self
9 | def lookup_by_content_type(mime); end
10 | def lookup_by_extension(extension); end
11 | def lookup_by_filename(filename); end
12 | end
13 | end
14 |
15 | module MiniMime::Configuration
16 | class << self
17 | def content_type_db_path; end
18 | def content_type_db_path=(_arg0); end
19 | def ext_db_path; end
20 | def ext_db_path=(_arg0); end
21 | end
22 | end
23 |
24 | class MiniMime::Db
25 | def initialize; end
26 |
27 | def lookup_by_content_type(content_type); end
28 | def lookup_by_extension(extension); end
29 |
30 | class << self
31 | def lookup_by_content_type(content_type); end
32 | def lookup_by_extension(extension); end
33 | def lookup_by_filename(filename); end
34 | end
35 | end
36 |
37 | class MiniMime::Db::Cache
38 | def initialize(size); end
39 |
40 | def []=(key, val); end
41 | def fetch(key, &blk); end
42 | end
43 |
44 | MiniMime::Db::LOCK = T.let(T.unsafe(nil), Thread::Mutex)
45 |
46 | class MiniMime::Db::RandomAccessDb
47 | def initialize(path, sort_order); end
48 |
49 | def lookup(val); end
50 | def lookup_uncached(val); end
51 | def resolve(row); end
52 | end
53 |
54 | MiniMime::Db::RandomAccessDb::MAX_CACHED = T.let(T.unsafe(nil), Integer)
55 |
56 | class MiniMime::Info
57 | def initialize(buffer); end
58 |
59 | def [](idx); end
60 | def binary?; end
61 | def content_type; end
62 | def content_type=(_arg0); end
63 | def encoding; end
64 | def encoding=(_arg0); end
65 | def extension; end
66 | def extension=(_arg0); end
67 | end
68 |
69 | MiniMime::Info::BINARY_ENCODINGS = T.let(T.unsafe(nil), Array)
70 |
71 | MiniMime::VERSION = T.let(T.unsafe(nil), String)
72 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/racc@1.5.2.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `racc` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | ParseError = Racc::ParseError
8 |
9 | Racc::Copyright = T.let(T.unsafe(nil), String)
10 |
11 | class Racc::Parser
12 | def _racc_do_parse_rb(arg, in_debug); end
13 | def _racc_do_reduce(arg, act); end
14 | def _racc_evalact(act, arg); end
15 | def _racc_init_sysvars; end
16 | def _racc_setup; end
17 | def _racc_yyparse_rb(recv, mid, arg, c_debug); end
18 | def next_token; end
19 | def on_error(t, val, vstack); end
20 | def racc_accept; end
21 | def racc_e_pop(state, tstack, vstack); end
22 | def racc_next_state(curstate, state); end
23 | def racc_print_stacks(t, v); end
24 | def racc_print_states(s); end
25 | def racc_read_token(t, tok, val); end
26 | def racc_reduce(toks, sim, tstack, vstack); end
27 | def racc_shift(tok, tstack, vstack); end
28 | def racc_token2str(tok); end
29 | def token_to_str(t); end
30 | def yyaccept; end
31 | def yyerrok; end
32 | def yyerror; end
33 |
34 | class << self
35 | def racc_runtime_type; end
36 | end
37 | end
38 |
39 | Racc::Parser::Racc_Main_Parsing_Routine = T.let(T.unsafe(nil), Symbol)
40 |
41 | Racc::Parser::Racc_Runtime_Core_Id_C = T.let(T.unsafe(nil), String)
42 |
43 | Racc::Parser::Racc_Runtime_Core_Version = T.let(T.unsafe(nil), String)
44 |
45 | Racc::Parser::Racc_Runtime_Core_Version_C = T.let(T.unsafe(nil), String)
46 |
47 | Racc::Parser::Racc_Runtime_Core_Version_R = T.let(T.unsafe(nil), String)
48 |
49 | Racc::Parser::Racc_Runtime_Type = T.let(T.unsafe(nil), String)
50 |
51 | Racc::Parser::Racc_Runtime_Version = T.let(T.unsafe(nil), String)
52 |
53 | Racc::Parser::Racc_YY_Parse_Method = T.let(T.unsafe(nil), Symbol)
54 |
55 | Racc::VERSION = T.let(T.unsafe(nil), String)
56 |
57 | Racc::Version = T.let(T.unsafe(nil), String)
58 |
--------------------------------------------------------------------------------
/lib/packwerk/configuration.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "pathname"
5 | require "yaml"
6 |
7 | module Packwerk
8 | class Configuration
9 | class << self
10 | def from_path(path = Dir.pwd)
11 | raise ArgumentError, "#{File.expand_path(path)} does not exist" unless File.exist?(path)
12 |
13 | default_packwerk_path = File.join(path, DEFAULT_CONFIG_PATH)
14 |
15 | if File.file?(default_packwerk_path)
16 | from_packwerk_config(default_packwerk_path)
17 | else
18 | new
19 | end
20 | end
21 |
22 | private
23 |
24 | def from_packwerk_config(path)
25 | new(
26 | YAML.load_file(path) || {},
27 | config_path: path
28 | )
29 | end
30 | end
31 |
32 | DEFAULT_CONFIG_PATH = "packwerk.yml"
33 | DEFAULT_INCLUDE_GLOBS = ["**/*.{rb,rake,erb}"]
34 | DEFAULT_EXCLUDE_GLOBS = ["{bin,node_modules,script,tmp,vendor}/**/*"]
35 |
36 | attr_reader(
37 | :include, :exclude, :root_path, :package_paths, :custom_associations, :load_paths, :inflections_file,
38 | :config_path,
39 | )
40 |
41 | def initialize(configs = {}, config_path: nil)
42 | @include = configs["include"] || DEFAULT_INCLUDE_GLOBS
43 | @exclude = configs["exclude"] || DEFAULT_EXCLUDE_GLOBS
44 | root = config_path ? File.dirname(config_path) : "."
45 | @root_path = File.expand_path(root)
46 | @package_paths = configs["package_paths"] || "**/"
47 | @custom_associations = configs["custom_associations"] || []
48 | @load_paths = (configs["load_paths"] || []).uniq
49 | @inflections_file = File.expand_path(configs["inflections_file"] || "config/inflections.yml", @root_path)
50 | @parallel = configs.key?("parallel") ? configs["parallel"] : true
51 |
52 | @config_path = config_path
53 | end
54 |
55 | def parallel?
56 | @parallel
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/packwerk/const_node_inspector.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | # Extracts a constant name from an AST node of type :const
6 | class ConstNodeInspector
7 | extend T::Sig
8 | include ConstantNameInspector
9 |
10 | sig do
11 | override
12 | .params(node: AST::Node, ancestors: T::Array[AST::Node])
13 | .returns(T.nilable(String))
14 | end
15 | def constant_name_from_node(node, ancestors:)
16 | return nil unless Node.constant?(node)
17 | parent = ancestors.first
18 | return nil unless root_constant?(parent)
19 |
20 | if parent && constant_in_module_or_class_definition?(node, parent: parent)
21 | fully_qualify_constant(ancestors)
22 | else
23 | begin
24 | Node.constant_name(node)
25 | rescue Node::TypeError
26 | nil
27 | end
28 | end
29 | end
30 |
31 | private
32 |
33 | # Only process the root `const` node for namespaced constant references. For example, in the
34 | # reference `Spam::Eggs::Thing`, we only process the const node associated with `Spam`.
35 | sig { params(parent: T.nilable(AST::Node)).returns(T::Boolean) }
36 | def root_constant?(parent)
37 | !(parent && Node.constant?(parent))
38 | end
39 |
40 | sig { params(node: AST::Node, parent: AST::Node).returns(T.nilable(T::Boolean)) }
41 | def constant_in_module_or_class_definition?(node, parent:)
42 | parent_name = Node.module_name_from_definition(parent)
43 | parent_name && parent_name == Node.constant_name(node)
44 | end
45 |
46 | sig { params(ancestors: T::Array[AST::Node]).returns(String) }
47 | def fully_qualify_constant(ancestors)
48 | # We're defining a class with this name, in which case the constant is implicitly fully qualified by its
49 | # enclosing namespace
50 | "::" + Node.parent_module_name(ancestors: ancestors)
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/test/support/application_fixture_helper.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | module ApplicationFixtureHelper
5 | TEMP_FIXTURE_DIR = ROOT.join("tmp", "fixtures").to_s
6 | DEFAULT_TEMPLATE = :minimal
7 |
8 | def setup_application_fixture
9 | @old_working_dir = Dir.pwd
10 | end
11 |
12 | def teardown_application_fixture
13 | Dir.chdir(@old_working_dir)
14 | FileUtils.remove_entry(@app_dir, true) if using_template?
15 | end
16 |
17 | def use_template(template)
18 | raise "use_template may only be called once per test" if using_template?
19 | copy_dir("test/fixtures/#{template}")
20 | Dir.chdir(app_dir)
21 | end
22 |
23 | def app_dir
24 | unless using_template?
25 | raise "You need to set up an application fixture by calling `use_template(:the_template)`."
26 | end
27 |
28 | @app_dir
29 | end
30 |
31 | def config
32 | @config ||= Packwerk::Configuration.from_path(app_dir)
33 | end
34 |
35 | def to_app_path(relative_path)
36 | File.join(app_dir, relative_path)
37 | end
38 |
39 | def to_app_paths(*relative_paths)
40 | relative_paths.map { |path| to_app_path(path) }
41 | end
42 |
43 | def merge_into_app_yaml_file(relative_path, hash)
44 | path = to_app_path(relative_path)
45 | YamlFile.new(path).merge(hash)
46 | end
47 |
48 | def remove_app_entry(relative_path)
49 | FileUtils.remove_entry(to_app_path(relative_path))
50 | end
51 |
52 | def open_app_file(*path, mode: "w+")
53 | expanded_path = to_app_path(File.join(*path))
54 | File.open(expanded_path, mode) { |file| yield file }
55 | end
56 |
57 | private
58 |
59 | def using_template?
60 | defined? @app_dir
61 | end
62 |
63 | def copy_dir(path)
64 | root = FileUtils.mkdir_p(fixture_path).last
65 | FileUtils.cp_r("#{path}/.", root)
66 | @app_dir = root
67 | end
68 |
69 | def fixture_path
70 | File.join(TEMP_FIXTURE_DIR, "#{name}-#{Time.now.strftime("%Y_%m_%d_%H_%M_%S")}")
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/test/unit/reference_offense_test.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | class ReferenceOffenseTest < Minitest::Test
8 | include FactoryHelper
9 |
10 | setup do
11 | destination_package = Package.new(name: "destination_package", config: { "enforce_privacy" => true })
12 | @reference = build_reference(destination_package: destination_package)
13 | end
14 |
15 | test "has its file attribute set to the relative path of the reference" do
16 | offense = ReferenceOffense.new(reference: @reference, violation_type: ViolationType::Privacy)
17 | assert_equal(@reference.relative_path, offense.file)
18 | end
19 |
20 | test "generates a sensible message for privacy violations" do
21 | offense = ReferenceOffense.new(reference: @reference, violation_type: ViolationType::Privacy)
22 |
23 | assert_match(
24 | "Privacy violation: '::SomeName' is private to 'destination_package' but referenced from " \
25 | "'components/source'.", offense.message
26 | )
27 | end
28 |
29 | test "generates a sensible message for dependency violations" do
30 | offense = ReferenceOffense.new(reference: @reference, violation_type: ViolationType::Dependency)
31 |
32 | expected = <<~EXPECTED
33 | Dependency violation: ::SomeName belongs to 'destination_package', but 'components/source' does not specify a dependency on 'destination_package'.
34 | Are we missing an abstraction?
35 | Is the code making the reference, and the referenced constant, in the right packages?
36 |
37 | Inference details: this is a reference to ::SomeName which seems to be defined in some/location.rb.
38 | To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations
39 | EXPECTED
40 |
41 | assert_equal(expected, offense.message)
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/packwerk.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | require "sorbet-runtime"
5 | require "active_support"
6 | require "fileutils"
7 |
8 | module Packwerk
9 | extend ActiveSupport::Autoload
10 |
11 | autoload :ApplicationLoadPaths
12 | autoload :ApplicationValidator
13 | autoload :AssociationInspector
14 | autoload :OffenseCollection
15 | autoload :Checker
16 | autoload :Cli
17 | autoload :Configuration
18 | autoload :ConstantDiscovery
19 | autoload :ConstantNameInspector
20 | autoload :ConstNodeInspector
21 | autoload :DependencyChecker
22 | autoload :DeprecatedReferences
23 | autoload :FileProcessor
24 | autoload :FilesForProcessing
25 | autoload :Graph
26 | autoload :Inflector
27 | autoload :Node
28 | autoload :NodeProcessor
29 | autoload :NodeProcessorFactory
30 | autoload :NodeVisitor
31 | autoload :Offense
32 | autoload :OffensesFormatter
33 | autoload :OutputStyle
34 | autoload :Package
35 | autoload :PackageSet
36 | autoload :ParsedConstantDefinitions
37 | autoload :Parsers
38 | autoload :ParseRun
39 | autoload :PrivacyChecker
40 | autoload :Reference
41 | autoload :ReferenceExtractor
42 | autoload :ReferenceOffense
43 | autoload :Result
44 | autoload :RunContext
45 | autoload :Version
46 | autoload :ViolationType
47 |
48 | module Inflections
49 | extend ActiveSupport::Autoload
50 |
51 | autoload :Custom
52 | autoload :Default
53 | end
54 |
55 | module OutputStyles
56 | extend ActiveSupport::Autoload
57 |
58 | autoload :Coloured
59 | autoload :Plain
60 | end
61 |
62 | autoload_under "commands" do
63 | autoload :OffenseProgressMarker
64 | end
65 |
66 | module Formatters
67 | extend ActiveSupport::Autoload
68 |
69 | autoload :OffensesFormatter
70 | autoload :ProgressFormatter
71 | end
72 |
73 | module Generators
74 | extend ActiveSupport::Autoload
75 |
76 | autoload :ConfigurationFile
77 | autoload :InflectionsFile
78 | autoload :RootPackage
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/test/unit/parsers/factory_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | # TODO: make better_html not require Rails
5 | require "rails/railtie"
6 |
7 | require "test_helper"
8 |
9 | module Packwerk
10 | module Parsers
11 | class FactoryTest < Minitest::Test
12 | test "#for_path gives ruby parser for common Ruby paths" do
13 | assert_instance_of(Parsers::Ruby, factory.for_path("foo.rb"))
14 | assert_instance_of(Parsers::Ruby, factory.for_path("relative/path/to/foo.ru"))
15 | assert_instance_of(Parsers::Ruby, factory.for_path("foo.rake"))
16 | assert_instance_of(Parsers::Ruby, factory.for_path("foo.builder"))
17 | assert_instance_of(Parsers::Ruby, factory.for_path("in/repo/gem/foo.gemspec"))
18 | assert_instance_of(Parsers::Ruby, factory.for_path("Gemfile"))
19 | assert_instance_of(Parsers::Ruby, factory.for_path("some/path/Rakefile"))
20 | end
21 |
22 | test "#for_path gives ERB parser for common ERB paths" do
23 | assert_instance_of(Parsers::Erb, factory.for_path("foo.html.erb"))
24 | assert_instance_of(Parsers::Erb, factory.for_path("foo.md.erb"))
25 | assert_instance_of(Parsers::Erb, factory.for_path("/sub/directory/foo.erb"))
26 |
27 | fake_class = Class.new
28 | with_erb_parser_class(fake_class) do
29 | assert_instance_of(fake_class, factory.for_path("foo.html.erb"))
30 | end
31 | end
32 |
33 | test "#for_path gives nil for unknown path" do
34 | assert_nil(factory.for_path("not_a_ruby.rb.txt"))
35 | assert_nil(factory.for_path("some/path/rb"))
36 | assert_nil(factory.for_path("compoennts/foo/body.erb.html"))
37 | end
38 |
39 | private
40 |
41 | def with_erb_parser_class(klass)
42 | factory.erb_parser_class = klass
43 | yield
44 | ensure
45 | factory.erb_parser_class = nil
46 | end
47 |
48 | def factory
49 | Parsers::Factory.instance
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/marcel@1.0.0.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `marcel` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | module Marcel
8 | end
9 |
10 | Marcel::EXTENSIONS = T.let(T.unsafe(nil), Hash)
11 |
12 | Marcel::MAGIC = T.let(T.unsafe(nil), Array)
13 |
14 | class Marcel::Magic
15 | def initialize(type); end
16 |
17 | def ==(other); end
18 | def audio?; end
19 | def child_of?(parent); end
20 | def comment; end
21 | def eql?(other); end
22 | def extensions; end
23 | def hash; end
24 | def image?; end
25 | def mediatype; end
26 | def subtype; end
27 | def text?; end
28 | def to_s; end
29 | def type; end
30 | def video?; end
31 |
32 | class << self
33 | def add(type, options); end
34 | def all_by_magic(io); end
35 | def by_extension(ext); end
36 | def by_magic(io); end
37 | def by_path(path); end
38 | def child?(child, parent); end
39 | def remove(type); end
40 |
41 | private
42 |
43 | def magic_match(io, method); end
44 | def magic_match_io(io, matches, buffer); end
45 | end
46 | end
47 |
48 | class Marcel::MimeType
49 | class << self
50 | def extend(type, extensions: T.unsafe(nil), parents: T.unsafe(nil), magic: T.unsafe(nil)); end
51 | def for(pathname_or_io = T.unsafe(nil), name: T.unsafe(nil), extension: T.unsafe(nil), declared_type: T.unsafe(nil)); end
52 |
53 | private
54 |
55 | def for_data(pathname_or_io); end
56 | def for_declared_type(declared_type); end
57 | def for_extension(extension); end
58 | def for_name(name); end
59 | def most_specific_type(from_magic_type, fallback_type); end
60 | def parse_media_type(content_type); end
61 | def root_types(type); end
62 | def with_io(pathname_or_io, &block); end
63 | end
64 | end
65 |
66 | Marcel::MimeType::BINARY = T.let(T.unsafe(nil), String)
67 |
68 | Marcel::TYPES = T.let(T.unsafe(nil), Hash)
69 |
70 | Marcel::VERSION = T.let(T.unsafe(nil), String)
71 |
--------------------------------------------------------------------------------
/test/unit/generators/configuration_file_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | module Generators
8 | class ConfigurationFileTest < Minitest::Test
9 | setup do
10 | @string_io = StringIO.new
11 | @temp_dir = Dir.mktmpdir
12 | @test_path = "the/path/we/want"
13 | end
14 |
15 | teardown do
16 | FileUtils.remove_entry(@temp_dir)
17 | end
18 |
19 | test ".generate creates a configuration file with load paths if available" do
20 | load_paths = [@test_path]
21 | generated_file_path = File.join(@temp_dir, Packwerk::Configuration::DEFAULT_CONFIG_PATH)
22 |
23 | assert(Packwerk::Generators::ConfigurationFile.generate(
24 | load_paths: load_paths,
25 | root: @temp_dir,
26 | out: @string_io
27 | ))
28 | assert(File.exist?(generated_file_path))
29 |
30 | contents = YAML.load_file(generated_file_path)
31 | assert_equal(load_paths, contents["load_paths"])
32 | end
33 |
34 | test ".generate creates a default configuration file if there were empty load paths array" do
35 | generated_file_path = File.join(@temp_dir, Packwerk::Configuration::DEFAULT_CONFIG_PATH)
36 | assert(Packwerk::Generators::ConfigurationFile.generate(load_paths: [], root: @temp_dir, out: @string_io))
37 | assert(File.exist?(generated_file_path))
38 | end
39 |
40 | test ".generate does not create a configuration file if a file exists" do
41 | file_path = File.join(@temp_dir, Packwerk::Configuration::DEFAULT_CONFIG_PATH)
42 | File.open(file_path, "w") do |_f|
43 | assert(Packwerk::Generators::ConfigurationFile.generate(
44 | load_paths: [@test_path],
45 | root: @temp_dir,
46 | out: @string_io
47 | ))
48 | assert_includes(@string_io.string, "configuration file already exists")
49 | end
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/websocket-extensions@0.1.5.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `websocket-extensions` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | module WebSocket
8 | end
9 |
10 | class WebSocket::Extensions
11 | def initialize; end
12 |
13 | def activate(header); end
14 | def add(ext); end
15 | def close; end
16 | def generate_offer; end
17 | def generate_response(header); end
18 | def process_incoming_message(message); end
19 | def process_outgoing_message(message); end
20 | def valid_frame_rsv(frame); end
21 | def valid_frame_rsv?(frame); end
22 |
23 | private
24 |
25 | def reserve(ext); end
26 | def reserved?(ext); end
27 | end
28 |
29 | class WebSocket::Extensions::ExtensionError < ::ArgumentError
30 | end
31 |
32 | WebSocket::Extensions::MESSAGE_OPCODES = T.let(T.unsafe(nil), Array)
33 |
34 | class WebSocket::Extensions::Parser
35 | class << self
36 | def parse_header(header); end
37 | def serialize_params(name, params); end
38 | end
39 | end
40 |
41 | WebSocket::Extensions::Parser::EXT = T.let(T.unsafe(nil), Regexp)
42 |
43 | WebSocket::Extensions::Parser::EXT_LIST = T.let(T.unsafe(nil), Regexp)
44 |
45 | WebSocket::Extensions::Parser::NOTOKEN = T.let(T.unsafe(nil), Regexp)
46 |
47 | WebSocket::Extensions::Parser::NUMBER = T.let(T.unsafe(nil), Regexp)
48 |
49 | WebSocket::Extensions::Parser::PARAM = T.let(T.unsafe(nil), Regexp)
50 |
51 | class WebSocket::Extensions::Parser::ParseError < ::ArgumentError
52 | end
53 |
54 | WebSocket::Extensions::Parser::QUOTED = T.let(T.unsafe(nil), Regexp)
55 |
56 | WebSocket::Extensions::Parser::TOKEN = T.let(T.unsafe(nil), Regexp)
57 |
58 | module WebSocket::Mask
59 | class << self
60 | def mask(_arg0, _arg1); end
61 | end
62 | end
63 |
64 | class WebSocket::Extensions::Offers
65 | def initialize; end
66 |
67 | def by_name(name); end
68 | def each_offer(&block); end
69 | def push(name, params); end
70 | def to_a; end
71 | end
72 |
--------------------------------------------------------------------------------
/packwerk.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "lib/packwerk/version"
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = "packwerk"
7 | spec.version = Packwerk::VERSION
8 | spec.authors = ["Shopify Inc."]
9 | spec.email = ["gems@shopify.com"]
10 |
11 | spec.summary = "Packages for applications based on the zeitwerk autoloader"
12 |
13 | spec.description = <<~DESCRIPTION
14 | Sets package level boundaries between a specified set of ruby
15 | constants to minimize cross-boundary referencing and dependency.
16 | DESCRIPTION
17 | spec.homepage = "https://github.com/Shopify/packwerk"
18 | spec.license = "MIT"
19 |
20 | if spec.respond_to?(:metadata)
21 | spec.metadata["homepage_uri"] = spec.homepage
22 | spec.metadata["source_code_uri"] = "https://github.com/Shopify/packwerk"
23 | spec.metadata["changelog_uri"] = "https://github.com/Shopify/packwerk/releases"
24 | end
25 |
26 | if spec.respond_to?(:metadata)
27 | spec.metadata["allowed_push_host"] = "https://rubygems.org"
28 | else
29 | raise "RubyGems 2.0 or newer is required to protect against " \
30 | "public gem pushes."
31 | end
32 |
33 | spec.bindir = "exe"
34 | spec.executables << "packwerk"
35 |
36 | spec.files = Dir.chdir(__dir__) do
37 | %x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features|static)/}) }
38 | end
39 | spec.require_paths = %w(lib)
40 |
41 | spec.required_ruby_version = ">= 2.4"
42 |
43 | spec.add_dependency("activesupport", ">= 5.2")
44 | spec.add_dependency("constant_resolver")
45 | spec.add_dependency("parallel")
46 | spec.add_dependency("sorbet-runtime")
47 |
48 | spec.add_development_dependency("bundler")
49 | spec.add_development_dependency("rake")
50 | spec.add_development_dependency("sorbet")
51 | spec.add_development_dependency("m")
52 |
53 | # For Ruby parsing
54 | spec.add_dependency("ast")
55 | spec.add_dependency("parser")
56 |
57 | # For ERB parsing
58 | spec.add_dependency("better_html")
59 | end
60 |
--------------------------------------------------------------------------------
/lib/packwerk/package_set.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "pathname"
5 |
6 | module Packwerk
7 | class PackageSet
8 | include Enumerable
9 |
10 | PACKAGE_CONFIG_FILENAME = "package.yml"
11 |
12 | class << self
13 | def load_all_from(root_path, package_pathspec: nil)
14 | package_paths = package_paths(root_path, package_pathspec || "**")
15 |
16 | packages = package_paths.map do |path|
17 | root_relative = path.dirname.relative_path_from(Pathname.new(root_path))
18 | Package.new(name: root_relative.to_s, config: YAML.load_file(path))
19 | end
20 |
21 | create_root_package_if_none_in(packages)
22 |
23 | new(packages)
24 | end
25 |
26 | def package_paths(root_path, package_pathspec)
27 | bundle_path_match = Bundler.bundle_path.join("**").to_s
28 |
29 | glob_patterns = Array(package_pathspec).map do |pathspec|
30 | File.join(root_path, pathspec, PACKAGE_CONFIG_FILENAME)
31 | end
32 |
33 | Dir.glob(glob_patterns)
34 | .map { |path| Pathname.new(path).cleanpath }
35 | .reject { |path| path.realpath.fnmatch(bundle_path_match) }
36 | end
37 |
38 | private
39 |
40 | def create_root_package_if_none_in(packages)
41 | return if packages.any?(&:root?)
42 | packages << Package.new(name: Package::ROOT_PACKAGE_NAME, config: nil)
43 | end
44 | end
45 |
46 | def initialize(packages)
47 | # We want to match more specific paths first
48 | sorted_packages = packages.sort_by { |package| -package.name.length }
49 | @packages = sorted_packages.each_with_object({}) { |package, hash| hash[package.name] = package }
50 | end
51 |
52 | def each(&blk)
53 | @packages.values.each(&blk)
54 | end
55 |
56 | def fetch(name)
57 | @packages[name]
58 | end
59 |
60 | def package_from_path(file_path)
61 | path_string = file_path.to_s
62 | @packages.values.find { |package| package.package_path?(path_string) }
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/test/unit/inflector_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | class InflectorTest < Minitest::Test
8 | def setup
9 | @inflector = inflector_for(file: "config/inflections.yml")
10 | end
11 |
12 | test "acts like activesupport inflector" do
13 | assert_operator Inflector.ancestors, :include?, ActiveSupport::Inflector
14 | end
15 |
16 | test "uses default inflections" do
17 | assert_equal(
18 | "Order",
19 | @inflector.classify("orders")
20 | )
21 |
22 | assert_equal(
23 | "Ox",
24 | @inflector.classify("oxen")
25 | )
26 | end
27 |
28 | test "#pluralize will pluralize when count not 1" do
29 | assert_equal "things", @inflector.pluralize("thing", 3)
30 | assert_equal "things", @inflector.pluralize("thing", -5)
31 | assert_equal "things", @inflector.pluralize("thing", 0)
32 | assert_equal "things", @inflector.pluralize("things", 1000)
33 | end
34 |
35 | test "#pluralize will singularize when count is 1" do
36 | assert_equal "thing", @inflector.pluralize("thing", 1)
37 | assert_equal "thing", @inflector.pluralize("things", 1)
38 | end
39 |
40 | test "#initialize will apply custom inflections from file" do
41 | inflector = inflector_for(file: "test/fixtures/skeleton/custom_inflections.yml")
42 |
43 | assert_equal "graphql", inflector.underscore("GraphQL")
44 | assert_equal "payment_details", inflector.singularize("payment_details")
45 | end
46 |
47 | test "#initialize will not apply custom inflections if there aren't any" do
48 | inflector = inflector_for(file: "no_inflections_here.yml")
49 |
50 | assert_equal "graph_ql", inflector.underscore("GraphQL")
51 | assert_equal "payment_detail", inflector.singularize("payment_details")
52 | end
53 |
54 | private
55 |
56 | def inflector_for(file:)
57 | custom_inflector = Packwerk::Inflections::Custom.new(file)
58 | Inflector.new(custom_inflector: custom_inflector)
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/packwerk/generators/configuration_file.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "erb"
5 |
6 | module Packwerk
7 | module Generators
8 | class ConfigurationFile
9 | extend T::Sig
10 |
11 | CONFIGURATION_TEMPLATE_FILE_PATH = "templates/packwerk.yml.erb"
12 |
13 | class << self
14 | def generate(load_paths:, root:, out:)
15 | new(load_paths: load_paths, root: root, out: out).generate
16 | end
17 | end
18 |
19 | sig { params(load_paths: T::Array[String], root: String, out: T.any(StringIO, IO)).void }
20 | def initialize(load_paths:, root:, out: $stdout)
21 | @load_paths = load_paths
22 | @root = root
23 | @out = out
24 |
25 | set_template_variables
26 | end
27 |
28 | sig { returns(T::Boolean) }
29 | def generate
30 | @out.puts("📦 Generating Packwerk configuration file...")
31 | default_config_path = File.join(@root, ::Packwerk::Configuration::DEFAULT_CONFIG_PATH)
32 |
33 | if File.exist?(default_config_path)
34 | @out.puts("⚠️ Packwerk configuration file already exists.")
35 | return true
36 | end
37 |
38 | File.write(default_config_path, render)
39 |
40 | @out.puts("✅ Packwerk configuration file generated in #{default_config_path}")
41 | true
42 | end
43 |
44 | private
45 |
46 | def set_template_variables
47 | @load_paths_formatted = if @load_paths.empty?
48 | "# load_paths:\n# - 'app/models'\n"
49 | else
50 | @load_paths.map { |path| "- #{path}\n" }.join
51 | end
52 |
53 | @load_paths_comment = unless @load_paths.empty?
54 | "# These load paths were auto generated by Packwerk.\nload_paths:\n"
55 | end
56 | end
57 |
58 | def render
59 | ERB.new(template, nil, "-").result(binding)
60 | end
61 |
62 | def template
63 | template_file_path = File.join(__dir__, CONFIGURATION_TEMPLATE_FILE_PATH)
64 | File.read(template_file_path)
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/packwerk/graph.rb:
--------------------------------------------------------------------------------
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
75 |
--------------------------------------------------------------------------------
/lib/packwerk/reference_offense.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | class ReferenceOffense < Offense
6 | extend T::Sig
7 | extend T::Helpers
8 |
9 | attr_reader :reference, :violation_type
10 |
11 | sig do
12 | params(
13 | reference: Packwerk::Reference,
14 | violation_type: Packwerk::ViolationType,
15 | location: T.nilable(Node::Location)
16 | )
17 | .void
18 | end
19 | def initialize(reference:, violation_type:, location: nil)
20 | super(file: reference.relative_path, message: build_message(reference, violation_type), location: location)
21 | @reference = reference
22 | @violation_type = violation_type
23 | end
24 |
25 | private
26 |
27 | def build_message(reference, violation_type)
28 | violation_message = case violation_type
29 | when ViolationType::Privacy
30 | source_desc = reference.source_package ? "'#{reference.source_package}'" : "here"
31 | "Privacy violation: '#{reference.constant.name}' is private to '#{reference.constant.package}' but " \
32 | "referenced from #{source_desc}.\n" \
33 | "Is there a public entrypoint in '#{reference.constant.package.public_path}' that you can use instead?"
34 | when ViolationType::Dependency
35 | "Dependency violation: #{reference.constant.name} belongs to '#{reference.constant.package}', but " \
36 | "'#{reference.source_package}' does not specify a dependency on " \
37 | "'#{reference.constant.package}'.\n" \
38 | "Are we missing an abstraction?\n" \
39 | "Is the code making the reference, and the referenced constant, in the right packages?\n"
40 | end
41 |
42 | <<~EOS
43 | #{violation_message}
44 | Inference details: this is a reference to #{reference.constant.name} which seems to be defined in #{reference.constant.location}.
45 | To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations
46 | EOS
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/nio4r@2.5.7.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `nio4r` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | module NIO
8 | class << self
9 | def engine; end
10 | def pure?(env = T.unsafe(nil)); end
11 | end
12 | end
13 |
14 | class NIO::ByteBuffer
15 | include(::Enumerable)
16 |
17 | def initialize(_arg0); end
18 |
19 | def <<(_arg0); end
20 | def [](_arg0); end
21 | def capacity; end
22 | def clear; end
23 | def compact; end
24 | def each; end
25 | def flip; end
26 | def full?; end
27 | def get(*_arg0); end
28 | def inspect; end
29 | def limit; end
30 | def limit=(_arg0); end
31 | def mark; end
32 | def position; end
33 | def position=(_arg0); end
34 | def read_from(_arg0); end
35 | def remaining; end
36 | def reset; end
37 | def rewind; end
38 | def size; end
39 | def write_to(_arg0); end
40 | end
41 |
42 | class NIO::ByteBuffer::MarkUnsetError < ::IOError
43 | end
44 |
45 | class NIO::ByteBuffer::OverflowError < ::IOError
46 | end
47 |
48 | class NIO::ByteBuffer::UnderflowError < ::IOError
49 | end
50 |
51 | NIO::ENGINE = T.let(T.unsafe(nil), String)
52 |
53 | class NIO::Monitor
54 | def initialize(_arg0, _arg1, _arg2); end
55 |
56 | def add_interest(_arg0); end
57 | def close(*_arg0); end
58 | def closed?; end
59 | def interests; end
60 | def interests=(_arg0); end
61 | def io; end
62 | def readable?; end
63 | def readiness; end
64 | def remove_interest(_arg0); end
65 | def selector; end
66 | def value; end
67 | def value=(_arg0); end
68 | def writable?; end
69 | def writeable?; end
70 | end
71 |
72 | class NIO::Selector
73 | def initialize(*_arg0); end
74 |
75 | def backend; end
76 | def close; end
77 | def closed?; end
78 | def deregister(_arg0); end
79 | def empty?; end
80 | def register(_arg0, _arg1); end
81 | def registered?(_arg0); end
82 | def select(*_arg0); end
83 | def wakeup; end
84 |
85 | class << self
86 | def backends; end
87 | end
88 | end
89 |
90 | NIO::VERSION = T.let(T.unsafe(nil), String)
91 |
--------------------------------------------------------------------------------
/test/unit/constant_discovery_test.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | class ConstantDiscoveryTest < Minitest::Test
8 | def setup
9 | @root_path = "test/fixtures/skeleton/"
10 | load_paths =
11 | Dir.glob(File.join(@root_path, "components/*/{app,test}/*{/concerns,}"))
12 | .map { |p| Pathname.new(p).relative_path_from(Pathname.new(@root_path)).to_s }
13 |
14 | @discovery = ConstantDiscovery.new(
15 | constant_resolver: ConstantResolver.new(root_path: @root_path, load_paths: load_paths),
16 | packages: PackageSet.load_all_from(@root_path)
17 | )
18 | super
19 | end
20 |
21 | test "discovers simple constant" do
22 | constant = @discovery.context_for("Order")
23 | assert_equal("::Order", constant.name)
24 | assert_equal("components/sales/app/models/order.rb", constant.location)
25 | assert_equal("components/sales", constant.package.name)
26 | assert_equal(false, constant.public?)
27 | end
28 |
29 | test "recognizes constants as public" do
30 | constant = @discovery.context_for("Sales::RecordNewOrder")
31 | assert_equal("::Sales::RecordNewOrder", constant.name)
32 | assert_equal("components/sales/app/public/sales/record_new_order.rb", constant.location)
33 | assert_equal("components/sales", constant.package.name)
34 | assert_equal(true, constant.public?)
35 | end
36 |
37 | test "raises with helpful message if there is a constant resolver error" do
38 | constant_resolver = stub
39 | constant_resolver.stubs(:resolve).raises(ConstantResolver::Error, "initial error message")
40 | discovery = ConstantDiscovery.new(
41 | constant_resolver: constant_resolver,
42 | packages: PackageSet.load_all_from(@root_path)
43 | )
44 |
45 | error = assert_raises(ConstantResolver::Error) do
46 | discovery.context_for("Sales::RecordNewOrder")
47 | end
48 |
49 | assert_equal(error.message, "initial error message\n Make sure autoload paths are added to the config file.")
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/test/unit/inflections/custom_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | module Inflections
8 | class CustomTest < Minitest::Test
9 | test "#initialize with nil inflection file returns empty inflections" do
10 | empty_inflection = Packwerk::Inflections::Custom.new
11 | assert_empty empty_inflection.inflections
12 | end
13 |
14 | test "#initialize with non-existant inflection file returns empty inflections" do
15 | empty_inflection = Packwerk::Inflections::Custom.new("this/file/doesn't exist")
16 | assert_empty empty_inflection.inflections
17 | end
18 |
19 | test "#initialize with an empty inflection file returns empty inflections" do
20 | Tempfile.create("test_file.yml") do |file|
21 | empty_inflection = Packwerk::Inflections::Custom.new(file.path)
22 | assert_empty empty_inflection.inflections
23 | end
24 | end
25 |
26 | test "#initialize with inflection file containing invalid keys raises exception" do
27 | Tempfile.create("test_file.yml") do |file|
28 | file.write("---\n:an_invalid_key:\n- value\n")
29 | file.flush
30 |
31 | assert_raises ArgumentError do
32 | Packwerk::Inflections::Custom.new(file.path)
33 | end
34 | end
35 | end
36 |
37 | test "#apply_to applies inflections" do
38 | inflections = ActiveSupport::Inflector::Inflections.new
39 |
40 | customs = Packwerk::Inflections::Custom.new
41 |
42 | customs.inflections = {
43 | plural: [%w(analysis analyses)],
44 | acronym: ["PKG"],
45 | singular: [[/status$/, "status"]],
46 | }
47 |
48 | customs.apply_to(inflections)
49 |
50 | assert_equal(
51 | [%w(analysis analyses)],
52 | inflections.plurals
53 | )
54 |
55 | assert_equal(
56 | { "pkg" => "PKG" },
57 | inflections.acronyms
58 | )
59 |
60 | assert_equal(
61 | [[/status$/, "status"]],
62 | inflections.singulars
63 | )
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/test/unit/parsers/erb_test.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | # TODO: make better_html not require Rails
5 | require "rails/railtie"
6 |
7 | require "test_helper"
8 |
9 | module Packwerk
10 | module Parsers
11 | class ErbTest < Minitest::Test
12 | test "#call returns node with valid file" do
13 | node = File.open(fixture_path("valid.erb"), "r") do |fixture|
14 | Erb.new.call(io: fixture)
15 | end
16 |
17 | assert_kind_of(::AST::Node, node)
18 | end
19 |
20 | test "#call writes parse error to stdout" do
21 | error_message = "stub error"
22 | err = Parser::SyntaxError.new(stub(message: error_message))
23 | parser = stub
24 | parser.stubs(:ast).raises(err)
25 |
26 | parser_class_stub = stub(new: parser)
27 |
28 | parser = Erb.new(parser_class: parser_class_stub)
29 | file_path = fixture_path("invalid.erb")
30 |
31 | exc = assert_raises(Parsers::ParseError) do
32 | File.open(file_path, "r") do |fixture|
33 | parser.call(io: fixture, file_path: file_path)
34 | end
35 | end
36 |
37 | assert_equal("Syntax error: stub error", exc.result.message)
38 | assert_equal(file_path, exc.result.file)
39 | end
40 |
41 | test "#call writes encoding error to stdout" do
42 | error_message = "stub error"
43 | err = EncodingError.new(error_message)
44 | parser = stub
45 | parser.stubs(:ast).raises(err)
46 |
47 | parser_class_stub = stub(new: parser)
48 |
49 | parser = Erb.new(parser_class: parser_class_stub)
50 | file_path = fixture_path("invalid.erb")
51 |
52 | exc = assert_raises(Parsers::ParseError) do
53 | File.open(file_path, "r") do |fixture|
54 | parser.call(io: fixture, file_path: file_path)
55 | end
56 | end
57 |
58 | assert_equal("stub error", exc.result.message)
59 | assert_equal(file_path.to_s, exc.result.file)
60 | end
61 |
62 | private
63 |
64 | def fixture_path(name)
65 | ROOT.join("test/fixtures/formats/erb", name).to_s
66 | end
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/method_source@1.0.0.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `method_source` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | module MethodSource
8 | extend(::MethodSource::CodeHelpers)
9 |
10 | class << self
11 | def comment_helper(source_location, name = T.unsafe(nil)); end
12 | def extract_code(source_location); end
13 | def lines_for(file_name, name = T.unsafe(nil)); end
14 | def source_helper(source_location, name = T.unsafe(nil)); end
15 | def valid_expression?(str); end
16 | end
17 | end
18 |
19 | module MethodSource::CodeHelpers
20 | def comment_describing(file, line_number); end
21 | def complete_expression?(str); end
22 | def expression_at(file, line_number, options = T.unsafe(nil)); end
23 |
24 | private
25 |
26 | def extract_first_expression(lines, consume = T.unsafe(nil), &block); end
27 | def extract_last_comment(lines); end
28 | end
29 |
30 | module MethodSource::CodeHelpers::IncompleteExpression
31 | class << self
32 | def ===(ex); end
33 | def rbx?; end
34 | end
35 | end
36 |
37 | MethodSource::CodeHelpers::IncompleteExpression::GENERIC_REGEXPS = T.let(T.unsafe(nil), Array)
38 |
39 | MethodSource::CodeHelpers::IncompleteExpression::RBX_ONLY_REGEXPS = T.let(T.unsafe(nil), Array)
40 |
41 | module MethodSource::MethodExtensions
42 | def comment; end
43 | def source; end
44 |
45 | class << self
46 | def included(klass); end
47 | end
48 | end
49 |
50 | module MethodSource::ReeSourceLocation
51 | def source_location; end
52 | end
53 |
54 | module MethodSource::SourceLocation
55 | end
56 |
57 | module MethodSource::SourceLocation::MethodExtensions
58 | def source_location; end
59 |
60 | private
61 |
62 | def trace_func(event, file, line, id, binding, classname); end
63 | end
64 |
65 | module MethodSource::SourceLocation::ProcExtensions
66 | def source_location; end
67 | end
68 |
69 | module MethodSource::SourceLocation::UnboundMethodExtensions
70 | def source_location; end
71 | end
72 |
73 | class MethodSource::SourceNotFoundError < ::StandardError
74 | end
75 |
76 | MethodSource::VERSION = T.let(T.unsafe(nil), String)
77 |
--------------------------------------------------------------------------------
/lib/packwerk/parsers/erb.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "ast/node"
5 | require "better_html"
6 | require "better_html/parser"
7 | require "parser/source/buffer"
8 |
9 | module Packwerk
10 | module Parsers
11 | class Erb
12 | def initialize(parser_class: BetterHtml::Parser, ruby_parser: Ruby.new)
13 | @parser_class = parser_class
14 | @ruby_parser = ruby_parser
15 | end
16 |
17 | def call(io:, file_path: "")
18 | buffer = Parser::Source::Buffer.new(file_path)
19 | buffer.source = io.read
20 | parse_buffer(buffer, file_path: file_path)
21 | end
22 |
23 | def parse_buffer(buffer, file_path:)
24 | parser = @parser_class.new(buffer, template_language: :html)
25 | to_ruby_ast(parser.ast, file_path)
26 | rescue EncodingError => e
27 | result = ParseResult.new(file: file_path, message: e.message)
28 | raise Parsers::ParseError, result
29 | rescue Parser::SyntaxError => e
30 | result = ParseResult.new(file: file_path, message: "Syntax error: #{e}")
31 | raise Parsers::ParseError, result
32 | end
33 |
34 | private
35 |
36 | def to_ruby_ast(erb_ast, file_path)
37 | # Note that we're not using the source location (line/column) at the moment, but if we did
38 | # care about that, we'd need to tweak this to insert empty lines and spaces so that things
39 | # line up with the ERB file
40 | code_pieces = code_nodes(erb_ast).map do |node|
41 | node.children.first
42 | end
43 |
44 | @ruby_parser.call(
45 | io: StringIO.new(code_pieces.join("\n")),
46 | file_path: file_path,
47 | )
48 | end
49 |
50 | def code_nodes(node)
51 | return enum_for(:code_nodes, node) unless block_given?
52 | return unless node.is_a?(::AST::Node)
53 |
54 | yield node if node.type == :code
55 |
56 | # Skip descending into an ERB comment node, which may contain code nodes
57 | if node.type == :erb
58 | first_child = node.children.first
59 | return if first_child&.type == :indicator && first_child&.children&.first == "#"
60 | end
61 |
62 | node.children.each do |child|
63 | code_nodes(child) { |n| yield n }
64 | end
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/packwerk/parsed_constant_definitions.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "ast/node"
5 |
6 | module Packwerk
7 | class ParsedConstantDefinitions
8 | def initialize(root_node:)
9 | @local_definitions = {}
10 |
11 | collect_local_definitions_from_root(root_node) if root_node
12 | end
13 |
14 | def local_reference?(constant_name, location: nil, namespace_path: [])
15 | qualifications = self.class.reference_qualifications(constant_name, namespace_path: namespace_path)
16 |
17 | qualifications.any? do |name|
18 | @local_definitions[name] &&
19 | @local_definitions[name] != location
20 | end
21 | end
22 |
23 | # What fully qualified constants can this constant refer to in this context?
24 | def self.reference_qualifications(constant_name, namespace_path:)
25 | return [constant_name] if constant_name.start_with?("::")
26 |
27 | fully_qualified_constant_name = "::#{constant_name}"
28 |
29 | possible_namespaces = namespace_path.each_with_object([""]) do |current, acc|
30 | acc << "#{acc.last}::#{current}" if acc.last && current
31 | end
32 |
33 | possible_namespaces.map { |namespace| namespace + fully_qualified_constant_name }
34 | end
35 |
36 | private
37 |
38 | def collect_local_definitions_from_root(node, current_namespace_path = [])
39 | if Node.constant_assignment?(node)
40 | add_definition(Node.constant_name(node), current_namespace_path, Node.name_location(node))
41 | elsif Node.module_name_from_definition(node)
42 | # handle compact constant nesting (e.g. "module Sales::Order")
43 | tempnode = node
44 | while (tempnode = Node.each_child(tempnode).find { |n| Node.constant?(n) })
45 | add_definition(Node.constant_name(tempnode), current_namespace_path, Node.name_location(tempnode))
46 | end
47 |
48 | current_namespace_path += Node.class_or_module_name(node).split("::")
49 | end
50 |
51 | Node.each_child(node) { |child| collect_local_definitions_from_root(child, current_namespace_path) }
52 | end
53 |
54 | def add_definition(constant_name, current_namespace_path, location)
55 | fully_qualified_constant = [""].concat(current_namespace_path).push(constant_name).join("::")
56 |
57 | @local_definitions[fully_qualified_constant] = location
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/packwerk/association_inspector.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | # Extracts the implicit constant reference from an active record association
6 | class AssociationInspector
7 | extend T::Sig
8 | include ConstantNameInspector
9 |
10 | CustomAssociations = T.type_alias { T.any(T::Array[Symbol], T::Set[Symbol]) }
11 |
12 | RAILS_ASSOCIATIONS = T.let(
13 | %i(
14 | belongs_to
15 | has_many
16 | has_one
17 | has_and_belongs_to_many
18 | ).to_set,
19 | CustomAssociations
20 | )
21 |
22 | sig { params(inflector: Inflector, custom_associations: CustomAssociations).void }
23 | def initialize(inflector:, custom_associations: Set.new)
24 | @inflector = inflector
25 | @associations = T.let(RAILS_ASSOCIATIONS + custom_associations, CustomAssociations)
26 | end
27 |
28 | sig do
29 | override
30 | .params(node: AST::Node, ancestors: T::Array[AST::Node])
31 | .returns(T.nilable(String))
32 | end
33 | def constant_name_from_node(node, ancestors:)
34 | return unless Node.method_call?(node)
35 | return unless association?(node)
36 |
37 | arguments = Node.method_arguments(node)
38 | return unless (association_name = association_name(arguments))
39 |
40 | if (class_name_node = custom_class_name(arguments))
41 | return unless Node.string?(class_name_node)
42 | Node.literal_value(class_name_node)
43 | else
44 | @inflector.classify(association_name.to_s)
45 | end
46 | end
47 |
48 | private
49 |
50 | sig { params(node: AST::Node).returns(T::Boolean) }
51 | def association?(node)
52 | method_name = Node.method_name(node)
53 | @associations.include?(method_name)
54 | end
55 |
56 | sig { params(arguments: T::Array[AST::Node]).returns(T.nilable(AST::Node)) }
57 | def custom_class_name(arguments)
58 | association_options = arguments.detect { |n| Node.hash?(n) }
59 | return unless association_options
60 |
61 | Node.value_from_hash(association_options, :class_name)
62 | end
63 |
64 | sig { params(arguments: T::Array[AST::Node]).returns(T.any(T.nilable(Symbol), T.nilable(String))) }
65 | def association_name(arguments)
66 | return unless Node.symbol?(arguments[0])
67 |
68 | Node.literal_value(arguments[0])
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/test/unit/parsers/ruby_test.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | module Parsers
8 | class RubyTest < Minitest::Test
9 | test "#call returns node with valid file" do
10 | node = File.open(fixture_path("valid.rb"), "r") do |fixture|
11 | Ruby.new.call(io: fixture)
12 | end
13 |
14 | assert_kind_of(::AST::Node, node)
15 | end
16 |
17 | test "#call writes parse error to stdout" do
18 | error_message = "stub error"
19 | err = Parser::SyntaxError.new(stub(message: error_message))
20 | parser = stub
21 | parser.stubs(:parse).raises(err)
22 |
23 | parser_class_stub = stub(new: parser)
24 |
25 | parser = Ruby.new(parser_class: parser_class_stub)
26 | file_path = fixture_path("invalid.rb")
27 |
28 | exc = assert_raises(Parsers::ParseError) do
29 | File.open(file_path, "r") do |fixture|
30 | parser.call(io: fixture, file_path: file_path)
31 | end
32 | end
33 |
34 | assert_equal("Syntax error: stub error", exc.result.message)
35 | assert_equal(file_path, exc.result.file)
36 | end
37 |
38 | test "#call writes encoding error to stdout" do
39 | error_message = "stub error"
40 | err = EncodingError.new(error_message)
41 | parser = stub
42 | parser.stubs(:parse).raises(err)
43 |
44 | parser_class_stub = stub(new: parser)
45 |
46 | parser = Ruby.new(parser_class: parser_class_stub)
47 | file_path = fixture_path("invalid.rb")
48 |
49 | exc = assert_raises(Parsers::ParseError) do
50 | File.open(file_path, "r") do |fixture|
51 | parser.call(io: fixture, file_path: file_path)
52 | end
53 | end
54 |
55 | assert_equal("stub error", exc.result.message)
56 | assert_equal(file_path, exc.result.file)
57 | end
58 |
59 | test "#call parses Ruby code containing invalid UTF-8 strings" do
60 | node = File.open(fixture_path("invalid_utf8_string.rb"), "r") do |fixture|
61 | Ruby.new.call(io: fixture)
62 | end
63 |
64 | assert_kind_of(::AST::Node, node)
65 | end
66 |
67 | private
68 |
69 | def fixture_path(name)
70 | ROOT.join("test/fixtures/formats/ruby", name).to_s
71 | end
72 | end
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/m@1.5.1.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `m` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | module M
8 | class << self
9 | def run(argv); end
10 | end
11 | end
12 |
13 | class M::Executor
14 | def initialize(testable); end
15 |
16 | def execute; end
17 |
18 | private
19 |
20 | def runner; end
21 | def suites; end
22 | def testable; end
23 | def tests; end
24 | end
25 |
26 | class M::Frameworks
27 | def framework_runner; end
28 |
29 | private
30 |
31 | def minitest4?; end
32 | def minitest5?; end
33 | def test_unit?; end
34 |
35 | class << self
36 | def framework_runner; end
37 | def minitest4?; end
38 | def minitest5?; end
39 | def test_unit?; end
40 | end
41 | end
42 |
43 | class M::Parser
44 | def initialize(argv); end
45 |
46 | def parse; end
47 |
48 | private
49 |
50 | def argv; end
51 | def parse_options!(argv); end
52 | def testable; end
53 | def wildcard(type); end
54 | end
55 |
56 | class M::Runner
57 | def initialize(argv); end
58 |
59 | def run; end
60 | end
61 |
62 | module M::Runners
63 | end
64 |
65 | class M::Runners::Base
66 | def run(_test_arguments); end
67 | def suites; end
68 | def test_methods(suite_class); end
69 | end
70 |
71 | class M::Runners::Minitest4 < ::M::Runners::Base
72 | def run(test_arguments); end
73 | def suites; end
74 | end
75 |
76 | class M::Runners::Minitest5 < ::M::Runners::Base
77 | def run(test_arguments); end
78 | def suites; end
79 | def test_methods(suite_class); end
80 | end
81 |
82 | class M::Runners::TestUnit < ::M::Runners::Base
83 | def run(test_arguments); end
84 | def suites; end
85 | def test_methods(suite_class); end
86 | end
87 |
88 | class M::Runners::UnsupportedFramework < ::M::Runners::Base
89 | def run(_test_arguments); end
90 | def suites; end
91 |
92 | private
93 |
94 | def not_supported; end
95 | end
96 |
97 | class M::Testable
98 | def initialize(file = T.unsafe(nil), lines = T.unsafe(nil), recursive = T.unsafe(nil)); end
99 |
100 | def file; end
101 | def file=(_); end
102 | def lines; end
103 | def lines=(lines); end
104 | def recursive; end
105 | def recursive=(_); end
106 | end
107 |
108 | M::VERSION = T.let(T.unsafe(nil), String)
109 |
--------------------------------------------------------------------------------
/lib/packwerk/reference_extractor.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | # extracts a possible constant reference from a given AST node
6 | class ReferenceExtractor
7 | extend T::Sig
8 |
9 | sig do
10 | params(
11 | context_provider: Packwerk::ConstantDiscovery,
12 | constant_name_inspectors: T::Array[Packwerk::ConstantNameInspector],
13 | root_node: ::AST::Node,
14 | root_path: String,
15 | ).void
16 | end
17 | def initialize(
18 | context_provider:,
19 | constant_name_inspectors:,
20 | root_node:,
21 | root_path:
22 | )
23 | @context_provider = context_provider
24 | @constant_name_inspectors = constant_name_inspectors
25 | @root_path = root_path
26 | @local_constant_definitions = ParsedConstantDefinitions.new(root_node: root_node)
27 | end
28 |
29 | def reference_from_node(node, ancestors:, file_path:)
30 | constant_name = T.let(nil, T.nilable(String))
31 |
32 | @constant_name_inspectors.each do |inspector|
33 | constant_name = inspector.constant_name_from_node(node, ancestors: ancestors)
34 | break if constant_name
35 | end
36 |
37 | reference_from_constant(constant_name, node: node, ancestors: ancestors, file_path: file_path) if constant_name
38 | end
39 |
40 | private
41 |
42 | def reference_from_constant(constant_name, node:, ancestors:, file_path:)
43 | namespace_path = Node.enclosing_namespace_path(node, ancestors: ancestors)
44 | return if local_reference?(constant_name, Node.name_location(node), namespace_path)
45 |
46 | constant =
47 | @context_provider.context_for(
48 | constant_name,
49 | current_namespace_path: namespace_path
50 | )
51 |
52 | return if constant&.package.nil?
53 |
54 | relative_path =
55 | Pathname.new(file_path)
56 | .relative_path_from(Pathname.new(@root_path)).to_s
57 |
58 | source_package = @context_provider.package_from_path(relative_path)
59 |
60 | return if source_package == constant.package
61 |
62 | Reference.new(source_package, relative_path, constant)
63 | end
64 |
65 | def local_reference?(constant_name, name_location, namespace_path)
66 | @local_constant_definitions.local_reference?(
67 | constant_name,
68 | location: name_location,
69 | namespace_path: namespace_path
70 | )
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/test/unit/offense_collection_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | class OffenseCollectionTest < Minitest::Test
8 | include FactoryHelper
9 |
10 | setup do
11 | @offense_collection = OffenseCollection.new(".")
12 | @offense = ReferenceOffense.new(reference: build_reference, violation_type: ViolationType::Dependency)
13 | end
14 |
15 | test "#add_violation adds entry and returns true" do
16 | Packwerk::DeprecatedReferences.any_instance
17 | .expects(:add_entries)
18 | .with(@offense.reference, @offense.violation_type)
19 |
20 | @offense_collection.add_offense(@offense)
21 | end
22 |
23 | test "#stale_violations? returns true if there are stale violations" do
24 | @offense_collection.add_offense(@offense)
25 |
26 | Packwerk::DeprecatedReferences.any_instance
27 | .expects(:stale_violations?)
28 | .returns(true)
29 |
30 | assert_predicate @offense_collection, :stale_violations?
31 | end
32 |
33 | test "#stale_violations? returns false if no stale violations" do
34 | @offense_collection.add_offense(@offense)
35 |
36 | Packwerk::DeprecatedReferences.any_instance
37 | .expects(:stale_violations?)
38 | .returns(false)
39 |
40 | refute_predicate @offense_collection, :stale_violations?
41 | end
42 |
43 | test "#listed? returns true if constant is listed in file" do
44 | package = Package.new(name: "buyer", config: {})
45 | reference = build_reference(source_package: package)
46 | deprecated_references = Packwerk::DeprecatedReferences.new(package, ".")
47 | deprecated_references
48 | .stubs(:listed?)
49 | .with(reference, violation_type: Packwerk::ViolationType::Dependency)
50 | .returns(true)
51 | Packwerk::DeprecatedReferences
52 | .stubs(:new)
53 | .with(package, "./buyer/deprecated_references.yml")
54 | .returns(deprecated_references)
55 |
56 | offense = Packwerk::ReferenceOffense.new(reference: reference, violation_type: ViolationType::Dependency)
57 | assert @offense_collection.listed?(offense)
58 | end
59 |
60 | test "#listed? returns false if constant is not listed in file " do
61 | offense = Packwerk::ReferenceOffense.new(reference: build_reference, violation_type: ViolationType::Dependency)
62 | refute @offense_collection.listed?(offense)
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/test/unit/association_inspector_test.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 | require "parser_test_helper"
6 |
7 | module Packwerk
8 | class AssociationInspectorTest < Minitest::Test
9 | setup do
10 | @inflector = Inflector.new(custom_inflector: Inflections::Custom.new)
11 | end
12 |
13 | test "#association? understands custom associations" do
14 | node = parse("has_lots :order")
15 | inspector = AssociationInspector.new(inflector: @inflector, custom_associations: [:has_lots])
16 |
17 | assert_equal "Order", inspector.constant_name_from_node(node, ancestors: [])
18 | end
19 |
20 | test "finds target constant for simple association" do
21 | node = parse("has_one :order")
22 | inspector = AssociationInspector.new(inflector: @inflector)
23 |
24 | assert_equal "Order", inspector.constant_name_from_node(node, ancestors: [])
25 | end
26 |
27 | test "finds target constant for association that pluralizes" do
28 | node = parse("has_many :orders")
29 | inspector = AssociationInspector.new(inflector: @inflector)
30 |
31 | assert_equal "Order", inspector.constant_name_from_node(node, ancestors: [])
32 | end
33 |
34 | test "finds target constant for association if explicitly specified" do
35 | node = parse("has_one :cool_order, { class_name: 'Order' }")
36 | inspector = AssociationInspector.new(inflector: @inflector)
37 |
38 | assert_equal "Order", inspector.constant_name_from_node(node, ancestors: [])
39 | end
40 |
41 | test "rejects method calls that are not associations" do
42 | node = parse('puts "Hello World"')
43 | inspector = AssociationInspector.new(inflector: @inflector)
44 |
45 | assert_nil inspector.constant_name_from_node(node, ancestors: [])
46 | end
47 |
48 | test "gives up on metaprogrammed associations" do
49 | node = parse("has_one association_name")
50 | inspector = AssociationInspector.new(inflector: @inflector)
51 |
52 | assert_nil inspector.constant_name_from_node(node, ancestors: [])
53 | end
54 |
55 | test "gives up on dynamic class name" do
56 | node = parse("has_one :order, class_name: Order.name")
57 | inspector = AssociationInspector.new(inflector: @inflector)
58 |
59 | assert_nil inspector.constant_name_from_node(node, ancestors: [])
60 | end
61 |
62 | private
63 |
64 | def parse(statement)
65 | ParserTestHelper.parse(statement)
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/test/unit/files_for_processing_test.rb:
--------------------------------------------------------------------------------
1 | # typed: false
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | class FilesForProcessingTest < Minitest::Test
8 | def setup
9 | @package_path = "components/sales"
10 | @configuration = ::Packwerk::Configuration.from_path("test/fixtures/skeleton")
11 | end
12 |
13 | test "fetch with custom paths includes only include glob in custom paths" do
14 | files = ::Packwerk::FilesForProcessing.fetch(paths: [@package_path], configuration: @configuration)
15 | included_file_pattern = File.join(@configuration.root_path, @package_path, "**/*.rb")
16 |
17 | assert_all_match(files, [included_file_pattern])
18 | end
19 |
20 | test "fetch with custom paths excludes the exclude glob in custom paths" do
21 | files = ::Packwerk::FilesForProcessing.fetch(paths: [@package_path], configuration: @configuration)
22 | excluded_file_pattern = File.join(@configuration.root_path, @package_path, "**/temp.rb")
23 |
24 | refute_any_match(files, [excluded_file_pattern])
25 | end
26 |
27 | test "fetch with no custom paths includes only include glob across codebase" do
28 | files = ::Packwerk::FilesForProcessing.fetch(paths: [], configuration: @configuration)
29 | included_file_patterns = @configuration.include.map { |pattern| File.join(@configuration.root_path, pattern) }
30 |
31 | assert_all_match(files, included_file_patterns)
32 | end
33 |
34 | test "fetch with no custom paths excludes the exclude glob across codebase" do
35 | files = ::Packwerk::FilesForProcessing.fetch(paths: [], configuration: @configuration)
36 | excluded_file_patterns = @configuration.exclude.map { |pattern| File.join(@configuration.root_path, pattern) }
37 |
38 | refute_any_match(files, excluded_file_patterns)
39 | end
40 |
41 | test "fetch does not return duplicated file paths" do
42 | files = ::Packwerk::FilesForProcessing.fetch(paths: [], configuration: @configuration)
43 | assert_equal files, files.uniq
44 | end
45 |
46 | private
47 |
48 | def assert_all_match(files, patterns)
49 | unmatched_files =
50 | files.reject do |file|
51 | patterns.any? do |pattern|
52 | File.fnmatch?(pattern, file, File::FNM_EXTGLOB)
53 | end
54 | end
55 |
56 | assert_empty(unmatched_files, "some files did not match inclusion patterns, #{patterns}")
57 | end
58 |
59 | def refute_any_match(files, patterns)
60 | matched_files =
61 | files.select do |file|
62 | patterns.any? do |pattern|
63 | File.fnmatch?(pattern, file, File::FNM_EXTGLOB)
64 | end
65 | end
66 |
67 | assert_empty(matched_files, "some files matched exclusion patterns, #{patterns}")
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/test/unit/configuration_test.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | class ConfigurationTest < Minitest::Test
8 | include ApplicationFixtureHelper
9 |
10 | setup do
11 | setup_application_fixture
12 | end
13 |
14 | teardown do
15 | teardown_application_fixture
16 | end
17 |
18 | test ".from_path raises ArgumentError if path does not exist" do
19 | File.expects(:exist?).with("foo").returns(false)
20 | error = assert_raises(ArgumentError) do
21 | Configuration.from_path("foo")
22 | end
23 |
24 | assert_equal("#{File.expand_path("foo")} does not exist", error.message)
25 | end
26 |
27 | test ".from_path uses packwerk config when it exists" do
28 | use_template(:minimal)
29 | remove_app_entry("packwerk.yml")
30 |
31 | configuration_hash = {
32 | "include" => ["xyz/*.rb"],
33 | "exclude" => ["{exclude_dir,bin,tmp}/**/*"],
34 | "package_paths" => "**/*/",
35 | "load_paths" => ["app/models"],
36 | "custom_associations" => ["custom_association"],
37 | "inflections_file" => "custom_inflections.yml",
38 | }
39 | merge_into_app_yaml_file("packwerk.yml", configuration_hash)
40 |
41 | configuration = Configuration.from_path(app_dir)
42 |
43 | assert_equal ["xyz/*.rb"], configuration.include
44 | assert_equal ["{exclude_dir,bin,tmp}/**/*"], configuration.exclude
45 | assert_equal app_dir, configuration.root_path
46 | assert_equal ["app/models"], configuration.load_paths
47 | assert_equal "**/*/", configuration.package_paths
48 | assert_equal ["custom_association"], configuration.custom_associations
49 | assert_equal to_app_path("custom_inflections.yml"), configuration.inflections_file
50 | end
51 |
52 | test ".from_path falls back to some default config when no existing config exists" do
53 | use_template(:minimal)
54 | remove_app_entry("packwerk.yml")
55 |
56 | configuration = Configuration.from_path
57 |
58 | assert_equal ["**/*.{rb,rake,erb}"], configuration.include
59 | assert_equal ["{bin,node_modules,script,tmp,vendor}/**/*"], configuration.exclude
60 | assert_equal app_dir, configuration.root_path
61 | assert_empty configuration.load_paths
62 | assert_equal "**/", configuration.package_paths
63 | assert_empty configuration.custom_associations
64 | assert_equal to_app_path("config/inflections.yml"), configuration.inflections_file
65 | end
66 |
67 | test ".from_path falls back to empty config when existing config is an empty document" do
68 | use_template(:blank)
69 | Configuration.expects(:new).with({}, config_path: to_app_path("packwerk.yml"))
70 | Configuration.from_path
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/lib/packwerk/offense_collection.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | module Packwerk
5 | class OffenseCollection
6 | extend T::Sig
7 | extend T::Helpers
8 |
9 | sig do
10 | params(
11 | root_path: String,
12 | deprecated_references: T::Hash[Packwerk::Package, Packwerk::DeprecatedReferences]
13 | ).void
14 | end
15 | def initialize(root_path, deprecated_references = {})
16 | @root_path = root_path
17 | @deprecated_references = T.let(deprecated_references, T::Hash[Packwerk::Package, Packwerk::DeprecatedReferences])
18 | @new_violations = T.let([], T::Array[Packwerk::ReferenceOffense])
19 | @errors = T.let([], T::Array[Packwerk::Offense])
20 | end
21 |
22 | sig { returns(T::Array[Packwerk::ReferenceOffense]) }
23 | attr_reader :new_violations
24 |
25 | sig { returns(T::Array[Packwerk::Offense]) }
26 | attr_reader :errors
27 |
28 | sig do
29 | params(offense: Packwerk::Offense)
30 | .returns(T::Boolean)
31 | end
32 | def listed?(offense)
33 | return false unless offense.is_a?(ReferenceOffense)
34 | reference = offense.reference
35 | deprecated_references_for(reference.source_package).listed?(reference, violation_type: offense.violation_type)
36 | end
37 |
38 | sig do
39 | params(offense: Packwerk::Offense).void
40 | end
41 | def add_offense(offense)
42 | unless offense.is_a?(ReferenceOffense)
43 | @errors << offense
44 | return
45 | end
46 | deprecated_references = deprecated_references_for(offense.reference.source_package)
47 | unless deprecated_references.add_entries(offense.reference, offense.violation_type)
48 | new_violations << offense
49 | end
50 | end
51 |
52 | sig { returns(T::Boolean) }
53 | def stale_violations?
54 | @deprecated_references.values.any?(&:stale_violations?)
55 | end
56 |
57 | sig { void }
58 | def dump_deprecated_references_files
59 | @deprecated_references.each do |_, deprecated_references_file|
60 | deprecated_references_file.dump
61 | end
62 | end
63 |
64 | sig { returns(T::Array[Packwerk::Offense]) }
65 | def outstanding_offenses
66 | errors + new_violations
67 | end
68 |
69 | private
70 |
71 | sig { params(package: Packwerk::Package).returns(Packwerk::DeprecatedReferences) }
72 | def deprecated_references_for(package)
73 | @deprecated_references[package] ||= Packwerk::DeprecatedReferences.new(
74 | package,
75 | deprecated_references_file_for(package),
76 | )
77 | end
78 |
79 | sig { params(package: Packwerk::Package).returns(String) }
80 | def deprecated_references_file_for(package)
81 | File.join(@root_path, package.name, "deprecated_references.yml")
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/test/unit/package_set_test.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | class PackageSetTest < Minitest::Test
8 | include RailsApplicationFixtureHelper
9 |
10 | setup do
11 | setup_application_fixture
12 | use_template(:skeleton)
13 | @package_set = PackageSet.load_all_from(app_dir)
14 | end
15 |
16 | teardown do
17 | teardown_application_fixture
18 | end
19 |
20 | test "#package_from_path returns package instance for a known path" do
21 | assert_equal("components/timeline", @package_set.package_from_path("components/timeline/something.rb").name)
22 | end
23 |
24 | test "#package_from_path returns root package for an unpackaged path" do
25 | assert_equal(".", @package_set.package_from_path("components/unknown/something.rb").name)
26 | end
27 |
28 | test "#package_from_path returns nested packages" do
29 | assert_equal(
30 | "components/timeline/nested",
31 | @package_set.package_from_path("components/timeline/nested/something.rb").name
32 | )
33 | end
34 |
35 | test "#fetch returns a package instance for known package name" do
36 | assert_equal("components/timeline", @package_set.fetch("components/timeline").name)
37 | end
38 |
39 | test "#fetch returns nil for unknown package name" do
40 | assert_nil(@package_set.fetch("components/unknown"))
41 | end
42 |
43 | test ".package_paths supports a path wildcard" do
44 | package_paths = PackageSet.package_paths(".", "**")
45 |
46 | assert_includes(package_paths, Pathname.new("components/sales/package.yml"))
47 | assert_includes(package_paths, Pathname.new("package.yml"))
48 | end
49 |
50 | test ".package_paths supports a single path as a string" do
51 | package_paths = PackageSet.package_paths(".", "components/sales")
52 |
53 | assert_equal(package_paths, [Pathname.new("components/sales/package.yml")])
54 | end
55 |
56 | test ".package_paths supports many paths as an array" do
57 | package_paths = PackageSet.package_paths(".", ["components/sales", "."])
58 |
59 | assert_equal(
60 | package_paths,
61 | [
62 | Pathname.new("components/sales/package.yml"),
63 | Pathname.new("package.yml"),
64 | ]
65 | )
66 | end
67 |
68 | test ".package_paths excludes paths inside the gem directory" do
69 | vendor_package_path = Pathname.new("vendor/cache/gems/example/package.yml")
70 |
71 | package_paths = PackageSet.package_paths(".", "**")
72 | assert_includes(package_paths, vendor_package_path)
73 |
74 | Bundler.expects(:bundle_path).returns(Rails.root.join("vendor/cache/gems"))
75 | package_paths = PackageSet.package_paths(".", "**")
76 | refute_includes(package_paths, vendor_package_path)
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/lib/packwerk/constant_discovery.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "constant_resolver"
5 |
6 | module Packwerk
7 | # Get information about (partially qualified) constants without loading the application code.
8 | # Information gathered: Fully qualified name, path to file containing the definition, package,
9 | # and visibility (public/private to the package).
10 | #
11 | # The implementation makes a few assumptions about the code base:
12 | # - `Something::SomeOtherThing` is defined in a path of either `something/some_other_thing.rb` or `something.rb`,
13 | # relative to the load path. Rails' `zeitwerk` autoloader makes the same assumption.
14 | # - It is OK to not always infer the exact file defining the constant. For example, when a constant is inherited, we
15 | # have no way of inferring the file it is defined in. You could argue though that inheritance means that another
16 | # constant with the same name exists in the inheriting class, and this view is sufficient for all our use cases.
17 | class ConstantDiscovery
18 | ConstantContext = Struct.new(:name, :location, :package, :public?)
19 |
20 | # @param constant_resolver [ConstantResolver]
21 | # @param packages [Packwerk::PackageSet]
22 | def initialize(constant_resolver:, packages:)
23 | @packages = packages
24 | @resolver = constant_resolver
25 | end
26 |
27 | # Get the package that owns a given file path.
28 | #
29 | # @param path [String] the file path
30 | #
31 | # @return [Packwerk::Package] the package that contains the given file,
32 | # or nil if the path is not owned by any component
33 | def package_from_path(path)
34 | @packages.package_from_path(path)
35 | end
36 |
37 | # Analyze a constant via its name.
38 | # If the name is partially qualified, we need the current namespace path to correctly infer its full name
39 | #
40 | # @param const_name [String] The constant's name, fully or partially qualified.
41 | # @param current_namespace_path [Array] (optional) The namespace of the context in which the constant is
42 | # used, e.g. ["Apps", "Models"] for `Apps::Models`. Defaults to [] which means top level.
43 | # @return [Packwerk::ConstantDiscovery::ConstantContext]
44 | def context_for(const_name, current_namespace_path: [])
45 | begin
46 | constant = @resolver.resolve(const_name, current_namespace_path: current_namespace_path)
47 | rescue ConstantResolver::Error => e
48 | raise(ConstantResolver::Error, e.message + "\n Make sure autoload paths are added to the config file.")
49 | end
50 |
51 | return unless constant
52 |
53 | package = @packages.package_from_path(constant.location)
54 | ConstantContext.new(
55 | constant.name,
56 | constant.location,
57 | package,
58 | package&.public_path?(constant.location),
59 | )
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/test/const_node_inspector_test.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 | require "parser_test_helper"
6 |
7 | module Packwerk
8 | class ConstNodeInspectorTest < ActiveSupport::TestCase
9 | setup do
10 | @inspector = ConstNodeInspector.new
11 | end
12 |
13 | test "#constant_name_from_node should ignore any non-const nodes" do
14 | node = parse("a = 1 + 1")
15 |
16 | constant_name = @inspector.constant_name_from_node(node, ancestors: [])
17 |
18 | assert_nil constant_name
19 | end
20 |
21 | test "#constant_name_from_node should return correct name for const node" do
22 | node = parse("Order")
23 |
24 | constant_name = @inspector.constant_name_from_node(node, ancestors: [])
25 |
26 | assert_equal "Order", constant_name
27 | end
28 |
29 | test "#constant_name_from_node should return correct name for fully-qualified const node" do
30 | node = parse("::Order")
31 |
32 | constant_name = @inspector.constant_name_from_node(node, ancestors: [])
33 |
34 | assert_equal "::Order", constant_name
35 | end
36 |
37 | test "#constant_name_from_node should return correct name for compact const node" do
38 | node = parse("Sales::Order")
39 |
40 | constant_name = @inspector.constant_name_from_node(node, ancestors: [])
41 |
42 | assert_equal "Sales::Order", constant_name
43 | end
44 |
45 | test "#constant_name_from_node should return correct name for simple class definition" do
46 | parent = parse("class Order; end")
47 | node = Node.each_child(parent).entries[0]
48 |
49 | constant_name = @inspector.constant_name_from_node(node, ancestors: [parent])
50 |
51 | assert_equal "::Order", constant_name
52 | end
53 |
54 | test "#constant_name_from_node should return correct name for nested and compact class definition" do
55 | grandparent = parse("module Foo::Bar; class Sales::Order; end; end")
56 | parent = Node.each_child(grandparent).entries[1] # module node; second child is the body of the module
57 | node = Node.each_child(parent).entries[0] # class node; first child is constant
58 |
59 | constant_name = @inspector.constant_name_from_node(node, ancestors: [parent, grandparent])
60 |
61 | assert_equal "::Foo::Bar::Sales::Order", constant_name
62 | end
63 |
64 | test "#constant_name_from_node should gracefully return nil for dynamically namespaced constants" do
65 | grandparent = parse("module CsvExportSharedTests; setup do self.class::HEADERS end; end")
66 | parent = Node.each_child(grandparent).entries[1] # setup do self.class::HEADERS end
67 | node = Node.each_child(parent).entries[2] # self.class::HEADERS
68 |
69 | constant_name = @inspector.constant_name_from_node(node, ancestors: [parent, grandparent])
70 |
71 | assert_nil constant_name
72 | end
73 |
74 | private
75 |
76 | def parse(code)
77 | ParserTestHelper.parse(code)
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/lib/packwerk/application_load_paths.rb:
--------------------------------------------------------------------------------
1 | # typed: strict
2 | # frozen_string_literal: true
3 |
4 | require "bundler"
5 |
6 | module Packwerk
7 | module ApplicationLoadPaths
8 | class << self
9 | extend T::Sig
10 |
11 | sig { params(root: String, environment: String).returns(T::Array[String]) }
12 | def extract_relevant_paths(root, environment)
13 | require_application(root, environment)
14 | all_paths = extract_application_autoload_paths
15 | relevant_paths = filter_relevant_paths(all_paths)
16 | assert_load_paths_present(relevant_paths)
17 | relative_path_strings(relevant_paths)
18 | end
19 |
20 | sig { returns(T::Array[String]) }
21 | def extract_application_autoload_paths
22 | Rails.application.railties
23 | .select { |railtie| railtie.is_a?(Rails::Engine) }
24 | .push(Rails.application)
25 | .flat_map do |engine|
26 | paths = (engine.config.autoload_paths + engine.config.eager_load_paths + engine.config.autoload_once_paths)
27 | paths.map(&:to_s).uniq
28 | end
29 | end
30 |
31 | sig do
32 | params(all_paths: T::Array[String], bundle_path: Pathname, rails_root: Pathname)
33 | .returns(T::Array[Pathname])
34 | end
35 | def filter_relevant_paths(all_paths, bundle_path: Bundler.bundle_path, rails_root: Rails.root)
36 | bundle_path_match = bundle_path.join("**")
37 | rails_root_match = rails_root.join("**")
38 |
39 | all_paths
40 | .map { |path| Pathname.new(path).expand_path }
41 | .select { |path| path.fnmatch(rails_root_match.to_s) } # path needs to be in application directory
42 | .reject { |path| path.fnmatch(bundle_path_match.to_s) } # reject paths from vendored gems
43 | end
44 |
45 | sig { params(paths: T::Array[Pathname], rails_root: Pathname).returns(T::Array[String]) }
46 | def relative_path_strings(paths, rails_root: Rails.root)
47 | paths
48 | .map { |path| path.relative_path_from(rails_root).to_s }
49 | .uniq
50 | end
51 |
52 | private
53 |
54 | sig { params(root: String, environment: String).void }
55 | def require_application(root, environment)
56 | environment_file = "#{root}/config/environment"
57 |
58 | if File.file?("#{environment_file}.rb")
59 | ENV["RAILS_ENV"] ||= environment
60 |
61 | require environment_file
62 | else
63 | raise "A Rails application could not be found in #{root}"
64 | end
65 | end
66 |
67 | sig { params(paths: T::Array[T.untyped]).void }
68 | def assert_load_paths_present(paths)
69 | if paths.empty?
70 | raise <<~EOS
71 | We could not extract autoload paths from your Rails app. This is likely a configuration error.
72 | Packwerk will not work correctly without any autoload paths.
73 | EOS
74 | end
75 | end
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/lib/packwerk/run_context.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "constant_resolver"
5 |
6 | module Packwerk
7 | class RunContext
8 | extend T::Sig
9 |
10 | attr_reader(
11 | :root_path,
12 | :load_paths,
13 | :package_paths,
14 | :inflector,
15 | :custom_associations,
16 | :checker_classes,
17 | )
18 |
19 | DEFAULT_CHECKERS = [
20 | ::Packwerk::DependencyChecker,
21 | ::Packwerk::PrivacyChecker,
22 | ]
23 |
24 | class << self
25 | def from_configuration(configuration)
26 | inflector = ::Packwerk::Inflector.from_file(configuration.inflections_file)
27 | new(
28 | root_path: configuration.root_path,
29 | load_paths: configuration.load_paths,
30 | package_paths: configuration.package_paths,
31 | inflector: inflector,
32 | custom_associations: configuration.custom_associations
33 | )
34 | end
35 | end
36 |
37 | def initialize(
38 | root_path:,
39 | load_paths:,
40 | package_paths: nil,
41 | inflector: nil,
42 | custom_associations: [],
43 | checker_classes: DEFAULT_CHECKERS
44 | )
45 | @root_path = root_path
46 | @load_paths = load_paths
47 | @package_paths = package_paths
48 | @inflector = inflector
49 | @custom_associations = custom_associations
50 | @checker_classes = checker_classes
51 | end
52 |
53 | sig { params(file: String).returns(T::Array[T.nilable(::Packwerk::Offense)]) }
54 | def process_file(file:)
55 | file_processor.call(file)
56 | end
57 |
58 | private
59 |
60 | sig { returns(FileProcessor) }
61 | def file_processor
62 | @file_processor ||= FileProcessor.new(node_processor_factory: node_processor_factory)
63 | end
64 |
65 | sig { returns(NodeProcessorFactory) }
66 | def node_processor_factory
67 | NodeProcessorFactory.new(
68 | context_provider: context_provider,
69 | checkers: checkers,
70 | root_path: root_path,
71 | constant_name_inspectors: constant_name_inspectors
72 | )
73 | end
74 |
75 | sig { returns(ConstantDiscovery) }
76 | def context_provider
77 | ::Packwerk::ConstantDiscovery.new(
78 | constant_resolver: resolver,
79 | packages: package_set
80 | )
81 | end
82 |
83 | sig { returns(ConstantResolver) }
84 | def resolver
85 | ConstantResolver.new(
86 | root_path: root_path,
87 | load_paths: load_paths,
88 | inflector: inflector,
89 | )
90 | end
91 |
92 | sig { returns(PackageSet) }
93 | def package_set
94 | ::Packwerk::PackageSet.load_all_from(root_path, package_pathspec: package_paths)
95 | end
96 |
97 | sig { returns(T::Array[Checker]) }
98 | def checkers
99 | checker_classes.map(&:new)
100 | end
101 |
102 | sig { returns(T::Array[ConstantNameInspector]) }
103 | def constant_name_inspectors
104 | [
105 | ::Packwerk::ConstNodeInspector.new,
106 | ::Packwerk::AssociationInspector.new(inflector: inflector, custom_associations: custom_associations),
107 | ]
108 | end
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/test/unit/privacy_checker_test.rb:
--------------------------------------------------------------------------------
1 | # typed: ignore
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 |
6 | module Packwerk
7 | class PrivacyCheckerTest < Minitest::Test
8 | include FactoryHelper
9 |
10 | test "ignores if destination package is not enforcing" do
11 | checker = privacy_checker
12 | reference = build_reference
13 |
14 | refute checker.invalid_reference?(reference)
15 | end
16 |
17 | test "ignores if destination package is only enforcing for other constants" do
18 | destination_package = Package.new(
19 | name: "destination_package",
20 | config: { "enforce_privacy" => ["::OtherConstant"] }
21 | )
22 | checker = privacy_checker
23 | reference = build_reference(destination_package: destination_package)
24 |
25 | refute checker.invalid_reference?(reference)
26 | end
27 |
28 | test "complains about private constant if enforcing privacy for everything" do
29 | destination_package = Package.new(name: "destination_package", config: { "enforce_privacy" => true })
30 | checker = privacy_checker
31 | reference = build_reference(destination_package: destination_package)
32 |
33 | assert checker.invalid_reference?(reference)
34 | end
35 |
36 | test "complains about private constant if enforcing for specific constants" do
37 | destination_package = Package.new(name: "destination_package", config: { "enforce_privacy" => ["::SomeName"] })
38 | checker = privacy_checker
39 | reference = build_reference(destination_package: destination_package)
40 |
41 | assert checker.invalid_reference?(reference)
42 | end
43 |
44 | test "complains about nested constant if enforcing for specific constants" do
45 | destination_package = Package.new(name: "destination_package", config: { "enforce_privacy" => ["::SomeName"] })
46 | checker = privacy_checker
47 | reference = build_reference(destination_package: destination_package)
48 |
49 | assert checker.invalid_reference?(reference)
50 | end
51 |
52 | test "ignores constant that starts like enforced constant" do
53 | destination_package = Package.new(name: "destination_package", config: { "enforce_privacy" => ["::SomeName"] })
54 | checker = privacy_checker
55 | reference = build_reference(destination_package: destination_package, constant_name: "::SomeNameButNotQuite")
56 |
57 | refute checker.invalid_reference?(reference)
58 | end
59 |
60 | test "ignores public constant even if enforcing privacy for everything" do
61 | destination_package = Package.new(name: "destination_package", config: { "enforce_privacy" => true })
62 | checker = privacy_checker
63 | reference = build_reference(destination_package: destination_package, public_constant: true)
64 |
65 | refute checker.invalid_reference?(reference)
66 | end
67 |
68 | test "only checks the deprecated references file for private constants" do
69 | destination_package = Package.new(name: "destination_package", config: { "enforce_privacy" => ["::SomeName"] })
70 | checker = privacy_checker
71 | reference = build_reference(destination_package: destination_package)
72 |
73 | checker.invalid_reference?(reference)
74 | end
75 |
76 | private
77 |
78 | def privacy_checker
79 | PrivacyChecker.new
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/test/unit/application_load_paths_test.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "test_helper"
5 | require "rails_test_helper"
6 |
7 | module Packwerk
8 | class ApplicationLoadPathsTest < Minitest::Test
9 | test ".relative_path_strings makes paths relative" do
10 | rails_root = Pathname.new("/application/")
11 | relative_path = "app/models"
12 | absolute_path = rails_root.join(relative_path)
13 | relative_path_strings = ApplicationLoadPaths.relative_path_strings(
14 | [absolute_path],
15 | rails_root: rails_root
16 | )
17 |
18 | assert_equal [relative_path], relative_path_strings
19 | end
20 |
21 | test ".filter_relevant_paths excludes paths outside of the application root" do
22 | valid_paths = ["/application/app/models"]
23 | paths = valid_paths + ["/users/tobi/.gems/something/app/models", "/application/../something/"]
24 | filtered_paths = ApplicationLoadPaths.filter_relevant_paths(
25 | paths,
26 | bundle_path: Pathname.new("/application/vendor/"),
27 | rails_root: Pathname.new("/application/")
28 | )
29 |
30 | assert_equal valid_paths, filtered_paths.map(&:to_s)
31 | end
32 |
33 | test ".filter_relevant_paths excludes paths from vendored gems" do
34 | valid_paths = ["/application/app/models"]
35 | paths = valid_paths + ["/application/vendor/something/app/models"]
36 | filtered_paths = ApplicationLoadPaths.filter_relevant_paths(
37 | paths,
38 | bundle_path: Pathname.new("/application/vendor/"),
39 | rails_root: Pathname.new("/application/")
40 | )
41 |
42 | assert_equal valid_paths, filtered_paths.map(&:to_s)
43 | end
44 |
45 | test ".extract_relevant_paths calls out to filter the paths" do
46 | ApplicationLoadPaths.expects(:filter_relevant_paths).once.returns([Pathname.new("/fake_path")])
47 | ApplicationLoadPaths.expects(:require_application).with("/application", "test").once.returns(true)
48 |
49 | ApplicationLoadPaths.extract_relevant_paths("/application", "test")
50 | end
51 |
52 | test ".extract_relevant_paths returns unique load paths" do
53 | path = Pathname.new("/application/app/models")
54 | ApplicationLoadPaths.expects(:filter_relevant_paths).once.returns([path, path])
55 | ApplicationLoadPaths.expects(:require_application).with("/application", "test").once.returns(true)
56 |
57 | assert_equal 1, ApplicationLoadPaths.extract_relevant_paths("/application", "test").count
58 | end
59 |
60 | test ".extract_application_autoload_paths returns unique autoload paths" do
61 | path = Pathname.new("/application/app/models")
62 | Rails.application.config.expects(:autoload_paths).once.returns([path])
63 | Rails.application.config.expects(:eager_load_paths).once.returns([path])
64 | Rails.application.config.expects(:autoload_once_paths).once.returns([path])
65 |
66 | assert_equal 1, ApplicationLoadPaths.extract_application_autoload_paths.count
67 | end
68 |
69 | test ".extract_application_autoload_paths returns autoload paths as strings" do
70 | path = Pathname.new("/application/app/models")
71 | Rails.application.config.expects(:autoload_paths).once.returns([path])
72 |
73 | assert_instance_of String, ApplicationLoadPaths.extract_application_autoload_paths.first
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/parallel@1.20.1.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `parallel` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | module Parallel
8 | extend(::Parallel::ProcessorCount)
9 |
10 | class << self
11 | def all?(*args, &block); end
12 | def any?(*args, &block); end
13 | def each(array, options = T.unsafe(nil), &block); end
14 | def each_with_index(array, options = T.unsafe(nil), &block); end
15 | def flat_map(*args, &block); end
16 | def in_processes(options = T.unsafe(nil), &block); end
17 | def in_threads(options = T.unsafe(nil)); end
18 | def map(source, options = T.unsafe(nil), &block); end
19 | def map_with_index(array, options = T.unsafe(nil), &block); end
20 | def worker_number; end
21 | def worker_number=(worker_num); end
22 |
23 | private
24 |
25 | def add_progress_bar!(job_factory, options); end
26 | def call_with_index(item, index, options, &block); end
27 | def create_workers(job_factory, options, &block); end
28 | def extract_count_from_options(options); end
29 | def process_incoming_jobs(read, write, job_factory, options, &block); end
30 | def replace_worker(job_factory, workers, i, options, blk); end
31 | def with_instrumentation(item, index, options); end
32 | def work_direct(job_factory, options, &block); end
33 | def work_in_processes(job_factory, options, &blk); end
34 | def work_in_threads(job_factory, options, &block); end
35 | def worker(job_factory, options, &block); end
36 | end
37 | end
38 |
39 | class Parallel::Break < ::StandardError
40 | def initialize(value = T.unsafe(nil)); end
41 |
42 | def value; end
43 | end
44 |
45 | class Parallel::DeadWorker < ::StandardError
46 | end
47 |
48 | class Parallel::ExceptionWrapper
49 | def initialize(exception); end
50 |
51 | def exception; end
52 | end
53 |
54 | class Parallel::JobFactory
55 | def initialize(source, mutex); end
56 |
57 | def next; end
58 | def pack(item, index); end
59 | def size; end
60 | def unpack(data); end
61 |
62 | private
63 |
64 | def producer?; end
65 | def queue_wrapper(array); end
66 | end
67 |
68 | class Parallel::Kill < ::Parallel::Break
69 | end
70 |
71 | module Parallel::ProcessorCount
72 | def physical_processor_count; end
73 | def processor_count; end
74 | end
75 |
76 | Parallel::Stop = T.let(T.unsafe(nil), Object)
77 |
78 | class Parallel::UndumpableException < ::StandardError
79 | def initialize(original); end
80 |
81 | def backtrace; end
82 | end
83 |
84 | class Parallel::UserInterruptHandler
85 | class << self
86 | def kill(thing); end
87 | def kill_on_ctrl_c(pids, options); end
88 |
89 | private
90 |
91 | def restore_interrupt(old, signal); end
92 | def trap_interrupt(signal); end
93 | end
94 | end
95 |
96 | Parallel::UserInterruptHandler::INTERRUPT_SIGNAL = T.let(T.unsafe(nil), Symbol)
97 |
98 | Parallel::VERSION = T.let(T.unsafe(nil), String)
99 |
100 | Parallel::Version = T.let(T.unsafe(nil), String)
101 |
102 | class Parallel::Worker
103 | def initialize(read, write, pid); end
104 |
105 | def close_pipes; end
106 | def pid; end
107 | def read; end
108 | def stop; end
109 | def thread; end
110 | def thread=(_arg0); end
111 | def work(data); end
112 | def write; end
113 |
114 | private
115 |
116 | def wait; end
117 | end
118 |
--------------------------------------------------------------------------------
/lib/packwerk/parse_run.rb:
--------------------------------------------------------------------------------
1 | # typed: true
2 | # frozen_string_literal: true
3 |
4 | require "benchmark"
5 | require "parallel"
6 |
7 | module Packwerk
8 | class ParseRun
9 | extend T::Sig
10 |
11 | def initialize(
12 | files:,
13 | configuration:,
14 | progress_formatter: Formatters::ProgressFormatter.new(StringIO.new),
15 | offenses_formatter: Formatters::OffensesFormatter.new
16 | )
17 | @configuration = configuration
18 | @progress_formatter = progress_formatter
19 | @offenses_formatter = offenses_formatter
20 | @files = files
21 | end
22 |
23 | def detect_stale_violations
24 | offense_collection = find_offenses
25 |
26 | result_status = !offense_collection.stale_violations?
27 | message = @offenses_formatter.show_stale_violations(offense_collection)
28 |
29 | Result.new(message: message, status: result_status)
30 | end
31 |
32 | def update_deprecations
33 | offense_collection = find_offenses
34 | offense_collection.dump_deprecated_references_files
35 |
36 | message = <<~EOS
37 | #{@offenses_formatter.show_offenses(offense_collection.errors)}
38 | ✅ `deprecated_references.yml` has been updated.
39 | EOS
40 |
41 | Result.new(message: message, status: offense_collection.errors.empty?)
42 | end
43 |
44 | def check
45 | offense_collection = find_offenses(show_errors: true)
46 |
47 | messages = [
48 | @offenses_formatter.show_offenses(offense_collection.outstanding_offenses),
49 | @offenses_formatter.show_stale_violations(offense_collection),
50 | ]
51 | result_status = offense_collection.outstanding_offenses.empty? && !offense_collection.stale_violations?
52 |
53 | Result.new(message: messages.join("\n") + "\n", status: result_status)
54 | end
55 |
56 | private
57 |
58 | def find_offenses(show_errors: false)
59 | offense_collection = OffenseCollection.new(@configuration.root_path)
60 | @progress_formatter.started(@files)
61 |
62 | run_context = Packwerk::RunContext.from_configuration(@configuration)
63 | all_offenses = T.let([], T.untyped)
64 |
65 | process_file = -> (path) do
66 | run_context.process_file(file: path).tap do |offenses|
67 | failed = show_errors && offenses.any? { |offense| !offense_collection.listed?(offense) }
68 | update_progress(failed: failed)
69 | end
70 | end
71 |
72 | execution_time = Benchmark.realtime do
73 | all_offenses = if @configuration.parallel?
74 | Parallel.flat_map(@files, &process_file)
75 | else
76 | serial_find_offenses(&process_file)
77 | end
78 | end
79 |
80 | @progress_formatter.finished(execution_time)
81 |
82 | all_offenses.each { |offense| offense_collection.add_offense(offense) }
83 | offense_collection
84 | end
85 |
86 | def serial_find_offenses
87 | all_offenses = T.let([], T.untyped)
88 | @files.each do |path|
89 | offenses = yield path
90 | all_offenses.concat(offenses)
91 | end
92 | all_offenses
93 | rescue Interrupt
94 | @progress_formatter.interrupted
95 | all_offenses
96 | end
97 |
98 | def update_progress(failed: false)
99 | if failed
100 | @progress_formatter.mark_as_failed
101 | else
102 | @progress_formatter.mark_as_inspected
103 | end
104 | end
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to make participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies within all project spaces, and it also applies when
49 | an individual is representing the project or its community in public spaces.
50 | Examples of representing a project or community include using an official
51 | project e-mail address, posting via an official social media account, or acting
52 | as an appointed representative at an online or offline event. Representation of
53 | a project may be further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at opensource@shopify.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/rails-dom-testing@2.0.3.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `rails-dom-testing` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | class HTMLSelector
8 | def initialize(values, previous_selection = T.unsafe(nil), &root_fallback); end
9 |
10 | def context; end
11 | def css_selector; end
12 | def message; end
13 | def select; end
14 | def selecting_no_body?; end
15 | def tests; end
16 |
17 | private
18 |
19 | def extract_equality_tests; end
20 | def extract_root(previous_selection, root_fallback); end
21 | def extract_selectors; end
22 | def filter(matches); end
23 |
24 | class << self
25 | def context; end
26 | end
27 | end
28 |
29 | HTMLSelector::NO_STRIP = T.let(T.unsafe(nil), Array)
30 |
31 | module Rails
32 | extend(::ActiveSupport::Autoload)
33 |
34 | class << self
35 | def app_class; end
36 | def app_class=(_); end
37 | def application; end
38 | def application=(_); end
39 | def autoloaders; end
40 | def backtrace_cleaner; end
41 | def cache; end
42 | def cache=(_); end
43 | def configuration; end
44 | def env; end
45 | def env=(environment); end
46 | def gem_version; end
47 | def groups(*groups); end
48 | def initialize!(*args, &block); end
49 | def initialized?(*args, &block); end
50 | def logger; end
51 | def logger=(_); end
52 | def public_path; end
53 | def root; end
54 | def version; end
55 | end
56 | end
57 |
58 | module Rails::Dom
59 | end
60 |
61 | module Rails::Dom::Testing
62 | end
63 |
64 | module Rails::Dom::Testing::Assertions
65 | include(::Rails::Dom::Testing::Assertions::DomAssertions)
66 | include(::Rails::Dom::Testing::Assertions::SelectorAssertions::CountDescribable)
67 | include(::Rails::Dom::Testing::Assertions::SelectorAssertions)
68 | extend(::ActiveSupport::Concern)
69 | end
70 |
71 | module Rails::Dom::Testing::Assertions::DomAssertions
72 | def assert_dom_equal(expected, actual, message = T.unsafe(nil)); end
73 | def assert_dom_not_equal(expected, actual, message = T.unsafe(nil)); end
74 |
75 | protected
76 |
77 | def compare_doms(expected, actual); end
78 | def equal_attribute?(attr, other_attr); end
79 | def equal_attribute_nodes?(nodes, other_nodes); end
80 | def equal_children?(child, other_child); end
81 |
82 | private
83 |
84 | def fragment(text); end
85 | end
86 |
87 | module Rails::Dom::Testing::Assertions::SelectorAssertions
88 | include(::Rails::Dom::Testing::Assertions::SelectorAssertions::CountDescribable)
89 |
90 | def assert_select(*args, &block); end
91 | def assert_select_email(&block); end
92 | def assert_select_encoded(element = T.unsafe(nil), &block); end
93 | def css_select(*args); end
94 |
95 | private
96 |
97 | def assert_size_match!(size, equals, css_selector, message = T.unsafe(nil)); end
98 | def document_root_element; end
99 | def nest_selection(selection); end
100 | def nodeset(node); end
101 | end
102 |
103 | module Rails::Dom::Testing::Assertions::SelectorAssertions::CountDescribable
104 | extend(::ActiveSupport::Concern)
105 |
106 |
107 | private
108 |
109 | def count_description(min, max, count); end
110 | def pluralize_element(quantity); end
111 | end
112 |
113 | class SubstitutionContext
114 | def initialize; end
115 |
116 | def match(matches, attribute, matcher); end
117 | def substitute!(selector, values, format_for_presentation = T.unsafe(nil)); end
118 |
119 | private
120 |
121 | def matcher_for(value, format_for_presentation); end
122 | def substitutable?(value); end
123 | end
124 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/rainbow@3.0.0.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `rainbow` gem.
3 | # Please instead update this file by running `dev typecheck update`.
4 |
5 | # typed: true
6 |
7 | module Rainbow
8 | class << self
9 | def enabled; end
10 | def enabled=(value); end
11 | def global; end
12 | def new; end
13 | def uncolor(string); end
14 | end
15 | end
16 |
17 | class Rainbow::Color
18 | def ground; end
19 |
20 | class << self
21 | def build(ground, values); end
22 | def parse_hex_color(hex); end
23 | end
24 | end
25 |
26 | class Rainbow::Color::Indexed < ::Rainbow::Color
27 | def initialize(ground, num); end
28 |
29 | def codes; end
30 | def num; end
31 | end
32 |
33 | class Rainbow::Color::Named < ::Rainbow::Color::Indexed
34 | def initialize(ground, name); end
35 |
36 | class << self
37 | def color_names; end
38 | def valid_names; end
39 | end
40 | end
41 |
42 | Rainbow::Color::Named::NAMES = T.let(T.unsafe(nil), Hash)
43 |
44 | class Rainbow::Color::RGB < ::Rainbow::Color::Indexed
45 | def initialize(ground, *values); end
46 |
47 | def b; end
48 | def codes; end
49 | def g; end
50 | def r; end
51 |
52 | private
53 |
54 | def code_from_rgb; end
55 |
56 | class << self
57 | def to_ansi_domain(value); end
58 | end
59 | end
60 |
61 | class Rainbow::Color::X11Named < ::Rainbow::Color::RGB
62 | include(::Rainbow::X11ColorNames)
63 |
64 | def initialize(ground, name); end
65 |
66 | class << self
67 | def color_names; end
68 | def valid_names; end
69 | end
70 | end
71 |
72 | class Rainbow::NullPresenter < ::String
73 | def background(*_values); end
74 | def bg(*_values); end
75 | def black; end
76 | def blink; end
77 | def blue; end
78 | def bold; end
79 | def bright; end
80 | def color(*_values); end
81 | def cyan; end
82 | def dark; end
83 | def faint; end
84 | def fg(*_values); end
85 | def foreground(*_values); end
86 | def green; end
87 | def hide; end
88 | def inverse; end
89 | def italic; end
90 | def magenta; end
91 | def method_missing(method_name, *args); end
92 | def red; end
93 | def reset; end
94 | def underline; end
95 | def white; end
96 | def yellow; end
97 |
98 | private
99 |
100 | def respond_to_missing?(method_name, *args); end
101 | end
102 |
103 | class Rainbow::Presenter < ::String
104 | def background(*values); end
105 | def bg(*values); end
106 | def black; end
107 | def blink; end
108 | def blue; end
109 | def bold; end
110 | def bright; end
111 | def color(*values); end
112 | def cyan; end
113 | def dark; end
114 | def faint; end
115 | def fg(*values); end
116 | def foreground(*values); end
117 | def green; end
118 | def hide; end
119 | def inverse; end
120 | def italic; end
121 | def magenta; end
122 | def method_missing(method_name, *args); end
123 | def red; end
124 | def reset; end
125 | def underline; end
126 | def white; end
127 | def yellow; end
128 |
129 | private
130 |
131 | def respond_to_missing?(method_name, *args); end
132 | def wrap_with_sgr(codes); end
133 | end
134 |
135 | Rainbow::Presenter::TERM_EFFECTS = T.let(T.unsafe(nil), Hash)
136 |
137 | class Rainbow::StringUtils
138 | class << self
139 | def uncolor(string); end
140 | def wrap_with_sgr(string, codes); end
141 | end
142 | end
143 |
144 | class Rainbow::Wrapper
145 | def initialize(enabled = T.unsafe(nil)); end
146 |
147 | def enabled; end
148 | def enabled=(_); end
149 | def wrap(string); end
150 | end
151 |
152 | module Rainbow::X11ColorNames
153 | end
154 |
155 | Rainbow::X11ColorNames::NAMES = T.let(T.unsafe(nil), Hash)
156 |
--------------------------------------------------------------------------------
/lib/packwerk/inflections/default.rb:
--------------------------------------------------------------------------------
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
74 |
--------------------------------------------------------------------------------
/sorbet/rbi/gems/spring@2.1.1.rbi:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT MANUALLY
2 | # This is an autogenerated file for types exported from the `spring` gem.
3 | # Please instead update this file by running `bin/tapioca sync`.
4 |
5 | # typed: true
6 |
7 | module Spring
8 | class << self
9 | def after_fork(&block); end
10 | def after_fork_callbacks; end
11 | def application_root; end
12 | def application_root=(_arg0); end
13 | def application_root_path; end
14 | def command(name); end
15 | def command?(name); end
16 | def commands; end
17 | def gemfile; end
18 | def project_root_path; end
19 | def quiet; end
20 | def quiet=(_arg0); end
21 | def register_command(name, command = T.unsafe(nil)); end
22 | def verify_environment; end
23 | def watch(*items); end
24 | def watch_interval; end
25 | def watch_interval=(_arg0); end
26 | def watch_method; end
27 | def watch_method=(method); end
28 | def watcher; end
29 | def watcher=(_arg0); end
30 |
31 | private
32 |
33 | def find_project_root(current_dir); end
34 | end
35 | end
36 |
37 | class Spring::ClientError < ::StandardError
38 | end
39 |
40 | class Spring::CommandNotFound < ::Spring::ClientError
41 | end
42 |
43 | class Spring::CommandWrapper
44 | def initialize(name, command = T.unsafe(nil)); end
45 |
46 | def binstub; end
47 | def binstub_name; end
48 | def call; end
49 | def command; end
50 | def description; end
51 | def env(args); end
52 | def exec; end
53 | def exec_name; end
54 | def gem_name; end
55 | def name; end
56 | def setup; end
57 | def setup?; end
58 | end
59 |
60 | module Spring::Commands
61 | end
62 |
63 | class Spring::Commands::Rails
64 | def call; end
65 | def description; end
66 | end
67 |
68 | class Spring::Commands::RailsConsole < ::Spring::Commands::Rails
69 | def command_name; end
70 | def env(args); end
71 | end
72 |
73 | class Spring::Commands::RailsDestroy < ::Spring::Commands::Rails
74 | def command_name; end
75 | end
76 |
77 | class Spring::Commands::RailsGenerate < ::Spring::Commands::Rails
78 | def command_name; end
79 | end
80 |
81 | class Spring::Commands::RailsRunner < ::Spring::Commands::Rails
82 | def call; end
83 | def command_name; end
84 | def env(args); end
85 | def extract_environment(args); end
86 | end
87 |
88 | class Spring::Commands::RailsTest < ::Spring::Commands::Rails
89 | def command_name; end
90 | def env(args); end
91 | end
92 |
93 | class Spring::Commands::Rake
94 | def env(args); end
95 |
96 | class << self
97 | def environment_matchers; end
98 | def environment_matchers=(_arg0); end
99 | end
100 | end
101 |
102 | class Spring::MissingApplication < ::Spring::ClientError
103 | def initialize(project_root); end
104 |
105 | def message; end
106 | def project_root; end
107 | end
108 |
109 | class Spring::UnknownProject < ::StandardError
110 | def initialize(current_dir); end
111 |
112 | def current_dir; end
113 | def message; end
114 | end
115 |
116 | module Spring::Watcher
117 | end
118 |
119 | class Spring::Watcher::Abstract
120 | include(::Mutex_m)
121 |
122 | def initialize(root, latency); end
123 |
124 | def add(*items); end
125 | def debug; end
126 | def directories; end
127 | def files; end
128 | def latency; end
129 | def lock; end
130 | def locked?; end
131 | def mark_stale; end
132 | def on_debug(&block); end
133 | def on_stale(&block); end
134 | def restart; end
135 | def root; end
136 | def stale?; end
137 | def start; end
138 | def stop; end
139 | def subjects_changed; end
140 | def synchronize(&block); end
141 | def try_lock; end
142 | def unlock; end
143 | end
144 |
145 | class Spring::Watcher::Polling < ::Spring::Watcher::Abstract
146 | def initialize(root, latency); end
147 |
148 | def add(*_arg0); end
149 | def check_stale; end
150 | def mtime; end
151 | def running?; end
152 | def start; end
153 | def stop; end
154 | def subjects_changed; end
155 |
156 | private
157 |
158 | def compute_mtime; end
159 | def expanded_files; end
160 | end
161 |
--------------------------------------------------------------------------------