├── .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 | 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 | --------------------------------------------------------------------------------