├── .github └── workflows │ ├── cd.yml │ ├── ci.yml │ ├── stale.yml │ └── triage.yml ├── .gitignore ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── Rakefile ├── bin ├── rake ├── rubocop └── tapioca ├── demo.txt ├── lib ├── packwerk-extensions.rb └── packwerk │ ├── folder_privacy │ ├── checker.rb │ ├── package.rb │ └── validator.rb │ ├── layer │ ├── checker.rb │ ├── config.rb │ ├── layers.rb │ ├── package.rb │ └── validator.rb │ ├── privacy │ ├── checker.rb │ ├── package.rb │ └── validator.rb │ └── visibility │ ├── checker.rb │ ├── package.rb │ └── validator.rb ├── packwerk-extensions.gemspec ├── sorbet ├── config ├── rbi │ └── gems │ │ ├── actioncable@7.0.4.rbi │ │ ├── actionmailbox@7.0.4.rbi │ │ ├── actionmailer@7.0.4.rbi │ │ ├── actionpack@7.0.4.rbi │ │ ├── actiontext@7.0.4.rbi │ │ ├── actionview@7.0.4.rbi │ │ ├── activejob@7.0.4.rbi │ │ ├── activemodel@7.0.4.rbi │ │ ├── activerecord@7.0.4.rbi │ │ ├── activestorage@7.0.4.rbi │ │ ├── activesupport@7.0.4.rbi │ │ ├── ast@2.4.2.rbi │ │ ├── better_html@2.0.1.rbi │ │ ├── builder@3.2.4.rbi │ │ ├── concurrent-ruby@1.1.10.rbi │ │ ├── constant_resolver@0.2.0.rbi │ │ ├── crass@1.0.6.rbi │ │ ├── diff-lcs@1.5.0.rbi │ │ ├── erubi@1.11.0.rbi │ │ ├── globalid@1.0.0.rbi │ │ ├── i18n@1.12.0.rbi │ │ ├── loofah@2.19.0.rbi │ │ ├── mail@2.7.1.rbi │ │ ├── marcel@1.0.2.rbi │ │ ├── method_source@1.0.0.rbi │ │ ├── mini_mime@1.1.2.rbi │ │ ├── minitest@5.16.3.rbi │ │ ├── mocha@1.16.0.rbi │ │ ├── net-imap@0.3.1.rbi │ │ ├── net-pop@0.1.2.rbi │ │ ├── net-protocol@0.1.3.rbi │ │ ├── net-smtp@0.3.3.rbi │ │ ├── netrc@0.11.0.rbi │ │ ├── nio4r@2.5.8.rbi │ │ ├── nokogiri@1.13.9.rbi │ │ ├── packwerk@2.2.1-8476bb3cd5452765ad452d36aa45ae724f1d44f5.rbi │ │ ├── parallel@1.22.1.rbi │ │ ├── parser@3.1.2.1.rbi │ │ ├── racc@1.6.0.rbi │ │ ├── rack-test@2.0.2.rbi │ │ ├── rack@2.2.4.rbi │ │ ├── rails-dom-testing@2.0.3.rbi │ │ ├── rails-html-sanitizer@1.4.3.rbi │ │ ├── rails@7.0.4.rbi │ │ ├── railties@7.0.4.rbi │ │ ├── rake@13.0.6.rbi │ │ ├── rbi@0.0.16.rbi │ │ ├── smart_properties@1.17.0.rbi │ │ ├── spoom@1.1.12.rbi │ │ ├── tapioca@0.7.3.rbi │ │ ├── thor@1.2.1.rbi │ │ ├── timeout@0.3.0.rbi │ │ ├── tzinfo@2.0.5.rbi │ │ ├── unparser@0.6.5.rbi │ │ ├── webrick@1.7.0.rbi │ │ ├── websocket-driver@0.7.5.rbi │ │ ├── websocket-extensions@0.1.5.rbi │ │ ├── yard-sorbet@0.7.0.rbi │ │ ├── yard@0.9.28.rbi │ │ └── zeitwerk@2.6.3.rbi └── tapioca │ ├── config.yml │ └── require.rb └── test ├── fixtures ├── extended │ ├── package.yml │ └── packwerk.yml ├── minimal │ ├── components │ │ └── sales │ │ │ ├── app │ │ │ ├── models │ │ │ │ ├── order.rb │ │ │ │ └── sales │ │ │ │ │ └── order.rb │ │ │ └── public │ │ │ │ └── sales │ │ │ │ └── record_new_order.rb │ │ │ └── package.yml │ ├── config │ │ └── environment.rb │ ├── package.yml │ ├── packwerk.yml │ └── utility │ │ └── package.yml ├── package_todo.yml ├── package_todo_with_conflicts.yml ├── skeleton │ ├── components │ │ ├── platform │ │ │ └── app │ │ │ │ └── models │ │ │ │ └── .gitkeep │ │ ├── sales │ │ │ ├── app │ │ │ │ ├── internal │ │ │ │ │ └── special.rb │ │ │ │ ├── models │ │ │ │ │ ├── order.rb │ │ │ │ │ ├── order │ │ │ │ │ │ ├── extension.rb │ │ │ │ │ │ └── foo.rb │ │ │ │ │ ├── payment_details.rb │ │ │ │ │ └── sales │ │ │ │ │ │ ├── entry.rb │ │ │ │ │ │ ├── order.rb │ │ │ │ │ │ ├── order │ │ │ │ │ │ └── error.rb │ │ │ │ │ │ └── temp.rb │ │ │ │ ├── public │ │ │ │ │ └── sales │ │ │ │ │ │ ├── errors.rb │ │ │ │ │ │ └── record_new_order.rb │ │ │ │ └── views │ │ │ │ │ └── order.html.erb │ │ │ ├── package.yml │ │ │ └── test │ │ │ │ └── unit │ │ │ │ └── order_test.rb │ │ └── timeline │ │ │ ├── app │ │ │ └── models │ │ │ │ ├── concerns │ │ │ │ └── has_timeline.rb │ │ │ │ ├── graphql.rb │ │ │ │ ├── graphql │ │ │ │ └── private_thing.rb │ │ │ │ ├── imaginary │ │ │ │ └── .gitkeep │ │ │ │ ├── private_thing.rb │ │ │ │ └── sales │ │ │ │ └── .gitkeep │ │ │ ├── nested │ │ │ └── package.yml │ │ │ └── package.yml │ ├── config │ │ ├── .gitkeep │ │ └── environment.rb │ ├── custom_inflections.yml │ ├── package.yml │ ├── packwerk.yml │ └── vendor │ │ └── cache │ │ └── gems │ │ └── example │ │ ├── models │ │ └── .gitkeep │ │ └── package.yml └── with_unrecognized_config │ ├── components │ └── sales │ │ ├── app │ │ ├── models │ │ │ ├── order.rb │ │ │ └── sales │ │ │ │ └── order.rb │ │ └── public │ │ │ └── sales │ │ │ └── record_new_order.rb │ │ └── package.yml │ ├── package.yml │ ├── packwerk.yml │ └── utility │ └── package.yml ├── integration ├── extension_test.rb └── extension_with_unrecognized_config_test.rb ├── rails_test_helper.rb ├── support ├── application_fixture_helper.rb ├── factory_helper.rb ├── rails_application_fixture_helper.rb ├── test_macro.rb └── yaml_file.rb ├── test_helper.rb └── unit ├── folder_privacy ├── checker_test.rb └── validator_test.rb ├── layer ├── checker_architecture_test.rb ├── checker_test.rb ├── config_test.rb ├── layers_test.rb └── validator_test.rb ├── privacy ├── checker_test.rb ├── package_test.rb └── validator_test.rb └── visibility ├── checker_test.rb └── validator_test.rb /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | workflow_run: 5 | workflows: [CI] 6 | types: [completed] 7 | branches: [main] 8 | 9 | jobs: 10 | call-workflow-from-shared-config: 11 | uses: rubyatscale/shared-config/.github/workflows/cd.yml@main 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | call-workflow-from-shared-config: 11 | uses: rubyatscale/shared-config/.github/workflows/ci.yml@main 12 | secrets: inherit 13 | with: 14 | test-command: "bundle exec rake" 15 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | jobs: 7 | call-workflow-from-shared-config: 8 | uses: rubyatscale/shared-config/.github/workflows/stale.yml@main 9 | -------------------------------------------------------------------------------- /.github/workflows/triage.yml: -------------------------------------------------------------------------------- 1 | name: Label issues as "triage" 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | jobs: 8 | call-workflow-from-shared-config: 9 | uses: rubyatscale/shared-config/.github/workflows/triage.yml@main 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /.bundle/ 10 | /tmp/ 11 | .rubocop-* 12 | .byebug_history 13 | sorbet/rbi/hidden-definitions/errors.txt 14 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # The behavior of RuboCop can be controlled via the .rubocop.yml 2 | # configuration file. It makes it possible to enable/disable 3 | # certain cops (checks) and to alter their behavior if they accept 4 | # any parameters. The file can be placed either in your home 5 | # directory or in some project directory. 6 | # 7 | # RuboCop will start looking for the configuration file in the directory 8 | # where the inspected file is and continue its way up to the root directory. 9 | # 10 | # See https://docs.rubocop.org/rubocop/configuration 11 | AllCops: 12 | SuggestExtensions: false 13 | NewCops: enable 14 | Exclude: 15 | - vendor/bundle/**/** 16 | 17 | Metrics/ParameterLists: 18 | Enabled: false 19 | 20 | # This cop is annoying with typed configuration 21 | Style/TrivialAccessors: 22 | Enabled: false 23 | 24 | # This rubocop is annoying when we use interfaces a lot 25 | Lint/UnusedMethodArgument: 26 | Enabled: false 27 | 28 | Gemspec/RequireMFA: 29 | Enabled: false 30 | 31 | Lint/DuplicateBranch: 32 | Enabled: false 33 | 34 | # If is sometimes easier to think about than unless sometimes 35 | Style/NegatedIf: 36 | Enabled: false 37 | 38 | # Disabling for now until it's clearer why we want this 39 | Style/FrozenStringLiteralComment: 40 | Enabled: false 41 | 42 | # It's nice to be able to read the condition first before reading the code within the condition 43 | Style/GuardClause: 44 | Enabled: false 45 | 46 | # 47 | # Leaving length metrics to human judgment for now 48 | # 49 | Metrics/ModuleLength: 50 | Enabled: false 51 | 52 | Layout/LineLength: 53 | Enabled: false 54 | 55 | Metrics/BlockLength: 56 | Enabled: false 57 | 58 | Metrics/MethodLength: 59 | Enabled: false 60 | 61 | Metrics/AbcSize: 62 | Enabled: false 63 | 64 | Metrics/ClassLength: 65 | Enabled: false 66 | 67 | # This doesn't feel useful 68 | Metrics/CyclomaticComplexity: 69 | Enabled: false 70 | 71 | # This doesn't feel useful 72 | Metrics/PerceivedComplexity: 73 | Enabled: false 74 | 75 | # It's nice to be able to read the condition first before reading the code within the condition 76 | Style/IfUnlessModifier: 77 | Enabled: false 78 | 79 | # This leads to code that is not very readable at times (very long lines) 80 | Style/ConditionalAssignment: 81 | Enabled: false 82 | 83 | # For now, we prefer to lean on clean method signatures as documentation. We may change this later. 84 | Style/Documentation: 85 | Enabled: false 86 | 87 | # Sometimes we leave comments in empty else statements intentionally 88 | Style/EmptyElse: 89 | Enabled: false 90 | 91 | # Sometimes we want to more explicitly list out a condition 92 | Style/RedundantCondition: 93 | Enabled: false 94 | 95 | # This leads to code that is not very readable at times (very long lines) 96 | Layout/MultilineMethodCallIndentation: 97 | Enabled: false 98 | 99 | # Blocks across lines are okay sometimes 100 | Style/BlockDelimiters: 101 | Enabled: false 102 | 103 | # Sometimes we like methods like `get_packages` 104 | Naming/AccessorMethodName: 105 | Enabled: false 106 | 107 | # This leads to code that is not very readable at times (very long lines) 108 | Layout/FirstArgumentIndentation: 109 | Enabled: false 110 | 111 | # This leads to code that is not very readable at times (very long lines) 112 | Layout/ArgumentAlignment: 113 | Enabled: false 114 | 115 | Style/AccessorGrouping: 116 | Enabled: false 117 | 118 | Style/NumericPredicate: 119 | Enabled: false 120 | 121 | Layout/BlockEndNewline: 122 | Enabled: false 123 | 124 | Naming/FileName: 125 | Exclude: 126 | - lib/packwerk-extensions.rb 127 | 128 | Lint/EmptyClass: 129 | Exclude: 130 | - test/**/* 131 | - tmp/**/* 132 | 133 | Lint/EmptyFile: 134 | Exclude: 135 | - test/fixtures/minimal/config/environment.rb 136 | 137 | Gemspec/DevelopmentDependencies: 138 | Enabled: false 139 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | See https://github.com/rubyatscale/packwerk-extensions/releases. 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at rubyatscale@gusto.com. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 125 | [https://www.contributor-covenant.org/translations][translations]. 126 | 127 | [homepage]: https://www.contributor-covenant.org 128 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 129 | [Mozilla CoC]: https://github.com/mozilla/diversity 130 | [FAQ]: https://www.contributor-covenant.org/faq 131 | [translations]: https://www.contributor-covenant.org/translations 132 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | gem 'packwerk', git: 'https://github.com/Shopify/packwerk', branch: 'main' 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/Shopify/packwerk 3 | revision: 6e57928e1508afd0f651ab68682c06b3a361d399 4 | branch: main 5 | specs: 6 | packwerk (2.2.1) 7 | activesupport (>= 6.0) 8 | ast 9 | better_html 10 | bundler 11 | constant_resolver (>= 0.2.0) 12 | parallel 13 | parser 14 | sorbet-runtime (>= 0.5.9914) 15 | zeitwerk (>= 2.6.1) 16 | 17 | PATH 18 | remote: . 19 | specs: 20 | packwerk-extensions (0.3.0) 21 | packwerk (>= 2.2.1) 22 | railties (>= 6.0.0) 23 | sorbet-runtime 24 | zeitwerk 25 | 26 | GEM 27 | remote: https://rubygems.org/ 28 | specs: 29 | actionpack (7.0.4.2) 30 | actionview (= 7.0.4.2) 31 | activesupport (= 7.0.4.2) 32 | rack (~> 2.0, >= 2.2.0) 33 | rack-test (>= 0.6.3) 34 | rails-dom-testing (~> 2.0) 35 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 36 | actionview (7.0.4.2) 37 | activesupport (= 7.0.4.2) 38 | builder (~> 3.1) 39 | erubi (~> 1.4) 40 | rails-dom-testing (~> 2.0) 41 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 42 | activesupport (7.0.4.2) 43 | concurrent-ruby (~> 1.0, >= 1.0.2) 44 | i18n (>= 1.6, < 2) 45 | minitest (>= 5.1) 46 | tzinfo (~> 2.0) 47 | ast (2.4.2) 48 | better_html (2.0.1) 49 | actionview (>= 6.0) 50 | activesupport (>= 6.0) 51 | ast (~> 2.0) 52 | erubi (~> 1.4) 53 | parser (>= 2.4) 54 | smart_properties 55 | builder (3.2.4) 56 | coderay (1.1.3) 57 | concurrent-ruby (1.2.0) 58 | constant_resolver (0.2.0) 59 | crass (1.0.6) 60 | diff-lcs (1.5.1) 61 | erubi (1.12.0) 62 | i18n (1.12.0) 63 | concurrent-ruby (~> 1.0) 64 | json (2.6.3) 65 | loofah (2.19.1) 66 | crass (~> 1.0.2) 67 | nokogiri (>= 1.5.9) 68 | method_source (1.0.0) 69 | minitest (5.17.0) 70 | mocha (2.0.2) 71 | ruby2_keywords (>= 0.0.5) 72 | nokogiri (1.16.4-arm64-darwin) 73 | racc (~> 1.4) 74 | nokogiri (1.16.4-x86_64-darwin) 75 | racc (~> 1.4) 76 | nokogiri (1.16.4-x86_64-linux) 77 | racc (~> 1.4) 78 | parallel (1.22.1) 79 | parser (3.2.1.0) 80 | ast (~> 2.4.1) 81 | pry (0.14.2) 82 | coderay (~> 1.1) 83 | method_source (~> 1.0) 84 | racc (1.6.2) 85 | rack (2.2.6.2) 86 | rack-test (2.0.2) 87 | rack (>= 1.3) 88 | rails-dom-testing (2.0.3) 89 | activesupport (>= 4.2.0) 90 | nokogiri (>= 1.6) 91 | rails-html-sanitizer (1.5.0) 92 | loofah (~> 2.19, >= 2.19.1) 93 | railties (7.0.4.2) 94 | actionpack (= 7.0.4.2) 95 | activesupport (= 7.0.4.2) 96 | method_source 97 | rake (>= 12.2) 98 | thor (~> 1.0) 99 | zeitwerk (~> 2.5) 100 | rainbow (3.1.1) 101 | rake (13.0.6) 102 | rbi (0.0.17) 103 | ast 104 | parser (>= 3.0.0) 105 | sorbet-runtime (>= 0.5.9204) 106 | unparser (>= 0.5.6) 107 | regexp_parser (2.7.0) 108 | rexml (3.2.5) 109 | rubocop (1.45.1) 110 | json (~> 2.3) 111 | parallel (~> 1.10) 112 | parser (>= 3.2.0.0) 113 | rainbow (>= 2.2.2, < 4.0) 114 | regexp_parser (>= 1.8, < 3.0) 115 | rexml (>= 3.2.5, < 4.0) 116 | rubocop-ast (>= 1.24.1, < 2.0) 117 | ruby-progressbar (~> 1.7) 118 | unicode-display_width (>= 2.4.0, < 3.0) 119 | rubocop-ast (1.26.0) 120 | parser (>= 3.2.1.0) 121 | ruby-progressbar (1.11.0) 122 | ruby2_keywords (0.0.5) 123 | smart_properties (1.17.0) 124 | sorbet (0.5.11353) 125 | sorbet-static (= 0.5.11353) 126 | sorbet-runtime (0.5.10672) 127 | sorbet-static (0.5.11353-universal-darwin) 128 | sorbet-static (0.5.11353-x86_64-linux) 129 | spoom (1.1.16) 130 | sorbet (>= 0.5.10187) 131 | sorbet-runtime (>= 0.5.9204) 132 | thor (>= 0.19.2) 133 | tapioca (0.7.3) 134 | bundler (>= 1.17.3) 135 | pry (>= 0.12.2) 136 | rbi (~> 0.0.0, >= 0.0.14) 137 | sorbet-runtime (>= 0.5.9204) 138 | sorbet-static (>= 0.5.9204) 139 | spoom (~> 1.1.0, >= 1.1.11) 140 | thor (>= 1.2.0) 141 | yard-sorbet 142 | thor (1.2.1) 143 | tzinfo (2.0.6) 144 | concurrent-ruby (~> 1.0) 145 | unicode-display_width (2.4.2) 146 | unparser (0.6.8) 147 | diff-lcs (~> 1.3) 148 | parser (>= 3.2.0) 149 | yard (0.9.36) 150 | yard-sorbet (0.8.1) 151 | sorbet-runtime (>= 0.5) 152 | yard (>= 0.9) 153 | zeitwerk (2.6.7) 154 | 155 | PLATFORMS 156 | universal-darwin 157 | x86_64-linux 158 | 159 | DEPENDENCIES 160 | minitest 161 | mocha 162 | packwerk! 163 | packwerk-extensions! 164 | pry 165 | rake 166 | rubocop 167 | sorbet 168 | sorbet-static 169 | tapioca 170 | 171 | BUNDLED WITH 172 | 2.5.9 173 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020-2022, Shopify Inc. 2 | Copyright 2023-present, Gusto 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # packwerk-extensions 2 | 3 | `packwerk-extensions` is a home for checker extensions for [packwerk](https://github.com/Shopify/packwerk) 3. 4 | 5 | Currently, it ships the following checkers to help improve the boundaries between packages. These checkers are: 6 | - A `privacy` checker that ensures other packages are using your package's public API 7 | - A `visibility` checker that allows packages to be private except to an explicit group of other packages. 8 | - A `folder_privacy` checker that allows packages to their sibling packs and parent pack (to be used in an application that uses folder packs) 9 | - A `layer` (formerly `architecture`) checker that allows packages to specify their "layer" and ensures they cannot use packages from higher layers 10 | 11 | ## Installation 12 | 13 | Add `packwerk-extensions` to your `Gemfile`. 14 | 15 | To register all checkers included in this gem, add the following to your `packwerk.yml`: 16 | 17 | ```yaml 18 | require: 19 | - packwerk-extensions 20 | ``` 21 | 22 | Alternatively, you can require individual checkers: 23 | 24 | ```yaml 25 | require: 26 | - packwerk/privacy/checker 27 | - packwerk/visibility/checker 28 | - packwerk/folder_privacy/checker 29 | - packwerk/layer/checker 30 | ``` 31 | 32 | ## Privacy Checker 33 | The privacy checker extension was originally extracted from [packwerk](https://github.com/Shopify/packwerk). 34 | 35 | A package's privacy boundary is violated when there is a reference to the package's private constants from a source outside the package. 36 | 37 | To enforce privacy for your package, set `enforce_privacy` to `true` or `strict` on your pack: 38 | 39 | ```yaml 40 | # components/merchandising/package.yml 41 | enforce_privacy: true 42 | ``` 43 | 44 | Setting `enforce_privacy` to `true` will make all references to private constants in your package a violation. 45 | 46 | Setting `enforce_privacy` to `strict` will forbid all references to private constants in your package. **This includes violations that have been added to other packages' `package_todo.yml` files.** 47 | 48 | Note: You will need to remove all existing privacy violations before setting `enforce_privacy` to `strict`. 49 | 50 | ### Using public folders 51 | You may enforce privacy either way mentioned above and still expose a public API for your package by placing constants in the public folder, which by default is `app/public`. The constants in the public folder will be made available for use by the rest of the application. 52 | 53 | ### Defining your own public folder 54 | 55 | You may prefer to override the default public folder, you can do so on a per-package basis by defining a `public_path`. 56 | 57 | Example: 58 | 59 | ```yaml 60 | public_path: my/custom/path/ 61 | ``` 62 | 63 | ### Defining public constants through sigil 64 | 65 | > [!WARNING] 66 | > This way of of defining the public API of a package should be considered WIP. It is not supported by all tooling in the RubyAtScale ecosystem, as @alexevanczuk pointed out in a [comment on the PR](https://github.com/rubyatscale/packwerk-extensions/pull/35#discussion_r1334331797): 67 | > 68 | > There are a couple of other places that will require changes related to this sigil. Namely, everything that is coupled to the public folder implementation of privacy. 69 | > 70 | > In the rubyatscale org: 71 | > 72 | > * pack_stats, example https://github.com/rubyatscale/pack_stats/blob/main/lib/pack_stats/private/metrics/public_usage.rb. (IMO though we can just remove this metric – it has never been useful) 73 | > * Other places that mention public_path or app/public. 74 | > * Org wide search for app/public link 75 | > * Org wide search for public_path link 76 | > * packs (the Rust port of packwerk – I could take this one over unless someone is interested in implementing whatever we come up with there 77 | 78 | 79 | 80 | You may make individual files public within a private package by usage of a comment within the first 5 lines of the `.rb` file containing `pack_public: true`. 81 | 82 | Example: 83 | 84 | ```ruby 85 | # pack_public: true 86 | module Foo 87 | class Update 88 | end 89 | end 90 | ``` 91 | Now `Foo::Update` is considered public even though the `foo` package might be set to `enforce_privacy: (true || strict)`. 92 | 93 | It's important to note that when combining `public_api: true` with the declaration of `private_constants`, 94 | `packwerk validate` will raise an exception if both are used for the same constant. This must be resolved by removing 95 | the sigil from the `.rb` file or removing the constant from the list of `private_constants`. 96 | 97 | If you are using rubocop, it may be configured in such a way that there must be an empty line after the magic keywords at the top of the file. Currently, this extension is not modifying rubocop in any way so it does not recognize `pack_public: true` as a valid magic keyword option. That means placing it at the end of the magic keywords will throw a rubocop exception. However, you can place it first in the list to avoid an exception in rubocop. 98 | ``` 99 | ----- 100 | # typed: ignore 101 | # frozen_string_literal: true 102 | # pack_public: true 103 | 104 | class Foo 105 | ... 106 | end => Layout/EmptyLineAfterMagicComment: Add an empty line after magic comments. 107 | 108 | ------ 109 | # typed: ignore 110 | # frozen_string_literal: true 111 | 112 | # pack_public: true 113 | 114 | class Foo 115 | ... 116 | end => Less than ideal. This won't raise an issue in rubocop, however, only the first 5 lines are scanned for the magic comment of pack_public so there is risk at it being missed. It also is requiring extra empty lines in the group of magic comments. 117 | 118 | ----- 119 | # pack_public: true 120 | # typed: ignore 121 | # frozen_string_literal: true 122 | 123 | class Foo 124 | ... 125 | end => Ideal solution. No exceptions from rubocop and very low risk of the magic comment being out of range since 126 | ``` 127 | 128 | ### Using specific private constants 129 | Sometimes it is desirable to only enforce privacy on a subset of constants in a package. You can do so by defining a `private_constants` list in your package.yml. Note that `enforce_privacy` must be set to `true` or `'strict'` for this to work. 130 | 131 | ### Ignore strict mode for violation coming from specific path patterns 132 | If you want to activate `'strict'` mode on your package but have a few privacy violations you know you will deal with later, 133 | you can set a list of patterns to exclude. 134 | 135 | ```yaml 136 | enforce_privacy: strict 137 | strict_privacy_ignored_patterns: 138 | - engines/another_engine/test/**/* 139 | ``` 140 | 141 | In this example, violations on constants of your engine referenced in those files `engines/another_engine/test/**/*` will not fail Packwerk checks. 142 | 143 | ### Package Privacy violation 144 | Packwerk thinks something is a privacy violation if you're referencing a constant, class, or module defined in the private implementation (i.e. not the public folder) of another package. We care about these because we want to make sure we only use parts of a package that have been exposed as public API. 145 | 146 | #### Interpreting Privacy violation 147 | 148 | > /Users/JaneDoe/src/github.com/sample-project/user/app/controllers/labels_controller.rb:170:30 149 | > Privacy violation: '::Billing::CarrierInvoiceTransaction' is private to 'billing' but referenced from 'user'. 150 | > Is there a public entrypoint in 'billing/app/public/' that you can use instead? 151 | > 152 | > Inference details: 'Billing::CarrierInvoiceTransaction' refers to ::Billing::CarrierInvoiceTransaction which seems to be defined in billing/app/models/billing/carrier_invoice_transaction.rb. 153 | 154 | There has been a privacy violation of the package `billing` in the package `user`, through the use of the constant `Billing::CarrierInvoiceTransaction` in the file `user/app/controllers/labels_controller.rb`. 155 | 156 | #### Suggestions 157 | You may be accessing the implementation of a piece of functionality that is supposed to be accessed through a public interface on the package. Try to use the public interface instead. A package’s public interface should be defined in its `app/public` folder and documented. 158 | 159 | The functionality you’re looking for may not be intended to be reused across packages at all. If there is no public interface for it but you have a good reason to use it from outside of its package, find the people responsible for the package and discuss a solution with them. 160 | 161 | ## Visibility Checker 162 | The visibility checker can be used to allow a package to be a private implementation detail of other packages. 163 | 164 | To enforce visibility for your package, set `enforce_visibility` to `true` on your pack and specify `visible_to` for other packages that can use your package. 165 | 166 | ```yaml 167 | # components/merchandising/package.yml 168 | enforce_visibility: true 169 | visible_to: 170 | - components/other_package 171 | ``` 172 | 173 | ## Folder-Visibility Checker 174 | The folder privacy checker can be used to allow a package to be private to their sibling packs and parent packs and will create todos if used by any other package. 175 | 176 | To enforce folder privacy for your package, set `enforce_folder_privacy` to `true` on your pack. 177 | 178 | ```yaml 179 | # components/merchandising/package.yml 180 | enforce_folder_privacy: true 181 | ``` 182 | 183 | Here is an example of paths and whether their use of `packs/b/packs/e` is OK or not, assuming that protects itself via `enforce_folder_privacy` 184 | 185 | ``` 186 | . OK (parent of parent) 187 | packs/a VIOLATION 188 | packs/b OK (parent) 189 | packs/b/packs/d OK (sibling) 190 | packs/b/packs/e ENFORCE_NESTED_VISIBILITY: TRUE 191 | packs/b/packs/e/packs/f VIOLATION 192 | packs/b/packs/e/packs/g VIOLATION 193 | packs/b/packs/h OK (sibling) 194 | packs/c VIOLATION 195 | ``` 196 | 197 | ## Layer Checker 198 | The layer checker can be used to define layers and then enforce constraints on the dependencies that packages can have. 199 | Dependencies are only allowed to packages in or below the same layer. 200 | 201 | To enforce layers for your package, first define the `layers` in `packwerk.yml`, for example: 202 | 203 | ``` 204 | layers: 205 | - feature 206 | - core 207 | - utility 208 | ``` 209 | 210 | Then, turn on the checker in your package: 211 | ```yaml 212 | # components/merchandising/package.yml 213 | enforce_layers: true 214 | layer: core 215 | ``` 216 | 217 | Now this pack can only depend on other packages from the `core` or `utility` layers. It cannot depend on packages from the higher `feature` layer. 218 | 219 | ### Deprecated Architecture Checker 220 | The "Layer Checker" was formerly named "Architecture Checker". The associated keys were: 221 | - packwerk.yml `architecture_layers`, which is now `layers` 222 | - package.yml `enforce_architecture`, which is now `enforce_layers` 223 | - package.yml `layer` is still a valid key 224 | - package_todo.yml - `architecture`, which is now `layer` 225 | 226 | ```bash 227 | # script to migrate code from deprecated "architecture" violations to "layer" violations 228 | # sed and ripgrep required 229 | 230 | # replace 'architecture_layers' with 'layers' in packwerk.yml 231 | sed -i '' 's/architecture_layers/layers/g' ./packwerk.yml 232 | 233 | # replace 'enforce_architecture' with 'enforce_layers' in package.yml files 234 | `rg -l 'enforce_architecture' -g 'package.yml' | xargs sed -i '' 's,enforce_architecture,enforce_layers,g'` 235 | 236 | # replace '- architecture' with '- layer' in package_todo.yml files 237 | `rg -l 'architecture' -g 'package_todo.yml' | xargs sed -i '' 's/- architecture/- layer/g'` 238 | ``` 239 | 240 | 241 | ## Contributing 242 | 243 | Got another checker you would like to add? Add it to this repo! 244 | 245 | Please ensure these commands pass for you locally: 246 | 247 | ``` 248 | bundle 249 | srb tc 250 | bin/rubocop 251 | bin/rake test 252 | ``` 253 | 254 | Then, submit a PR! 255 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # typed: ignore 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/gem_tasks' 5 | require 'rake/testtask' 6 | 7 | Rake::TestTask.new(:test) do |t| 8 | t.libs << 'test' 9 | t.libs << 'lib' 10 | t.test_files = FileList['test/**/*_test.rb'] 11 | t.warning = false 12 | end 13 | 14 | task(default: :test) 15 | -------------------------------------------------------------------------------- /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', __dir__) 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/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', __dir__) 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', __dir__) 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 | -------------------------------------------------------------------------------- /demo.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/demo.txt -------------------------------------------------------------------------------- /lib/packwerk-extensions.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | require 'sorbet-runtime' 5 | require 'packwerk' 6 | 7 | require 'packwerk/privacy/checker' 8 | require 'packwerk/visibility/checker' 9 | require 'packwerk/folder_privacy/checker' 10 | require 'packwerk/layer/checker' 11 | 12 | module Packwerk 13 | module Extensions 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/packwerk/folder_privacy/checker.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | require 'packwerk/folder_privacy/package' 5 | require 'packwerk/folder_privacy/validator' 6 | 7 | module Packwerk 8 | module FolderPrivacy 9 | class Checker 10 | extend T::Sig 11 | include Packwerk::Checker 12 | 13 | VIOLATION_TYPE = T.let('folder_privacy', String) 14 | 15 | sig { override.returns(String) } 16 | def violation_type 17 | VIOLATION_TYPE 18 | end 19 | 20 | sig do 21 | override 22 | .params(reference: Packwerk::Reference) 23 | .returns(T::Boolean) 24 | end 25 | def invalid_reference?(reference) 26 | referencing_package = reference.package 27 | referenced_package = reference.constant.package 28 | 29 | return false if enforcement_disabled?(Package.from(referenced_package).enforce_folder_privacy) 30 | 31 | # the root pack is parent folder of all packs, so we short-circuit this here 32 | referencing_package_is_root_pack = referencing_package.name == '.' 33 | return false if referencing_package_is_root_pack 34 | 35 | packages_are_sibling_folders = Pathname.new(referenced_package.name).dirname == Pathname.new(referencing_package.name).dirname 36 | return false if packages_are_sibling_folders 37 | 38 | referencing_package_is_parent_folder = Pathname.new(referenced_package.name).to_s.start_with?(referencing_package.name) 39 | return false if referencing_package_is_parent_folder 40 | 41 | true 42 | end 43 | 44 | sig do 45 | override 46 | .params(listed_offense: Packwerk::ReferenceOffense) 47 | .returns(T::Boolean) 48 | end 49 | def strict_mode_violation?(listed_offense) 50 | publishing_package = listed_offense.reference.constant.package 51 | publishing_package.config['enforce_folder_privacy'] == 'strict' 52 | end 53 | 54 | sig do 55 | override 56 | .params(reference: Packwerk::Reference) 57 | .returns(String) 58 | end 59 | def message(reference) 60 | source_desc = "'#{reference.package}'" 61 | 62 | message = <<~MESSAGE 63 | Folder Privacy violation: '#{reference.constant.name}' belongs to '#{reference.constant.package}', which is private to #{source_desc} as it is not a sibling pack or parent pack. 64 | Is there a different package to use instead, or should '#{reference.constant.package}' also be visible to #{source_desc}? 65 | 66 | #{standard_help_message(reference)} 67 | MESSAGE 68 | 69 | message.chomp 70 | end 71 | 72 | private 73 | 74 | sig do 75 | params(visibility_option: T.nilable(T.any(T::Boolean, String))) 76 | .returns(T::Boolean) 77 | end 78 | def enforcement_disabled?(visibility_option) 79 | [false, nil].include?(visibility_option) 80 | end 81 | 82 | sig { params(reference: Reference).returns(String) } 83 | def standard_help_message(reference) 84 | standard_message = <<~MESSAGE.chomp 85 | Inference details: this is a reference to #{reference.constant.name} which seems to be defined in #{reference.constant.location}. 86 | To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations 87 | MESSAGE 88 | 89 | standard_message.chomp 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/packwerk/folder_privacy/package.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | module Packwerk 5 | module FolderPrivacy 6 | class Package < T::Struct 7 | extend T::Sig 8 | 9 | const :enforce_folder_privacy, T.nilable(T.any(T::Boolean, String)) 10 | 11 | class << self 12 | extend T::Sig 13 | 14 | sig { params(package: ::Packwerk::Package).returns(Package) } 15 | def from(package) 16 | Package.new( 17 | enforce_folder_privacy: package.config['enforce_folder_privacy'] 18 | ) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/packwerk/folder_privacy/validator.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | module Packwerk 5 | module FolderPrivacy 6 | class Validator 7 | extend T::Sig 8 | include Packwerk::Validator 9 | 10 | Result = Packwerk::Validator::Result 11 | 12 | sig { override.params(package_set: PackageSet, configuration: Configuration).returns(Result) } 13 | def call(package_set, configuration) 14 | results = T.let([], T::Array[Result]) 15 | 16 | package_manifests_settings_for(configuration, 'enforce_folder_privacy').each do |config, setting| 17 | next if setting.nil? 18 | 19 | next if [TrueClass, FalseClass].include?(setting.class) || setting == 'strict' 20 | 21 | results << Result.new( 22 | ok: false, 23 | error_value: "\tInvalid 'enforce_folder_privacy' option: #{setting.inspect} in #{config.inspect}" 24 | ) 25 | end 26 | 27 | merge_results(results, separator: "\n---\n") 28 | end 29 | 30 | sig { override.returns(T::Array[String]) } 31 | def permitted_keys 32 | %w[enforce_folder_privacy] 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/packwerk/layer/checker.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | require 'packwerk/layer/config' 5 | require 'packwerk/layer/layers' 6 | require 'packwerk/layer/package' 7 | require 'packwerk/layer/validator' 8 | 9 | module Packwerk 10 | module Layer 11 | # This enforces "layered architecture," which allows each class to be designated as one of N layers 12 | # configured by the client in `packwerk.yml`, for example: 13 | # 14 | # layers: 15 | # - orchestrator 16 | # - business_domain 17 | # - platform 18 | # - utility 19 | # - specification 20 | # 21 | # Then a package can configure: 22 | # enforce_layers: true | false | strict 23 | # layer: utility 24 | # 25 | # This is intended to provide: 26 | # A) Direction for which dependency violations to tackle 27 | # B) What dependencies should or should not exist 28 | # C) A potential sequencing for modularizing a system (starting with lower layers first). 29 | # 30 | class Checker 31 | extend T::Sig 32 | include Packwerk::Checker 33 | 34 | sig { void } 35 | def initialize 36 | @violation_type = T.let(@violation_type, T.nilable(String)) 37 | end 38 | 39 | sig { override.returns(String) } 40 | def violation_type 41 | @violation_type ||= layer_config.violation_key 42 | end 43 | 44 | sig do 45 | override 46 | .params(reference: Packwerk::Reference) 47 | .returns(T::Boolean) 48 | end 49 | def invalid_reference?(reference) 50 | constant_package = Package.from(reference.constant.package, layers) 51 | referencing_package = Package.from(reference.package, layers) 52 | !referencing_package.can_depend_on?(constant_package, layers: layers) 53 | end 54 | 55 | sig do 56 | override 57 | .params(listed_offense: Packwerk::ReferenceOffense) 58 | .returns(T::Boolean) 59 | end 60 | def strict_mode_violation?(listed_offense) 61 | constant_package = listed_offense.reference.package 62 | constant_package.config[layer_config.enforce_key] == 'strict' 63 | end 64 | 65 | sig do 66 | override 67 | .params(reference: Packwerk::Reference) 68 | .returns(String) 69 | end 70 | def message(reference) 71 | constant_package = Package.from(reference.constant.package, layers) 72 | referencing_package = Package.from(reference.package, layers) 73 | 74 | message = <<~MESSAGE 75 | Layer violation: '#{reference.constant.name}' belongs to '#{reference.constant.package}', whose layer type is "#{constant_package.layer}". 76 | This constant cannot be referenced by '#{reference.package}', whose layer type is "#{referencing_package.layer}". 77 | Packs in a lower layer may not access packs in a higher layer. See the `layers` in packwerk.yml. Current hierarchy: 78 | - #{layers.names_list.join("\n- ")} 79 | 80 | #{standard_help_message(reference)} 81 | MESSAGE 82 | 83 | message.chomp 84 | end 85 | 86 | # TODO: Extract this out into a common helper, can call it StandardViolationHelpMessage.new(...) and implements .to_s 87 | sig { params(reference: Reference).returns(String) } 88 | def standard_help_message(reference) 89 | standard_message = <<~MESSAGE.chomp 90 | Inference details: this is a reference to #{reference.constant.name} which seems to be defined in #{reference.constant.location}. 91 | To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations 92 | MESSAGE 93 | 94 | standard_message.chomp 95 | end 96 | 97 | sig { returns(Layers) } 98 | def layers 99 | @layers ||= T.let(Layers.new, T.nilable(Packwerk::Layer::Layers)) 100 | end 101 | 102 | sig { returns(Config) } 103 | def layer_config 104 | @layer_config ||= T.let(Config.new, T.nilable(Config)) 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/packwerk/layer/config.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | module Packwerk 5 | module Layer 6 | class Config 7 | extend T::Sig 8 | 9 | ARCHITECTURE_VIOLATION_TYPE = T.let('architecture', String) 10 | ARCHITECTURE_ENFORCE = T.let('enforce_architecture', String) 11 | LAYER_VIOLATION_TYPE = T.let('layer', String) 12 | LAYER_ENFORCE = T.let('enforce_layers', String) 13 | 14 | sig { void } 15 | def initialize 16 | @layers_key_configured = T.let(@layers_key_configured, T.nilable(T::Boolean)) 17 | @layers_list = T.let(@layers_list, T.nilable(T::Array[String])) 18 | end 19 | 20 | sig { returns(T::Array[String]) } 21 | def layers_list 22 | @layers_list ||= YAML.load_file('packwerk.yml')[layers_key] || [] 23 | end 24 | 25 | sig { returns(T::Boolean) } 26 | def layers_key_configured? 27 | @layers_key_configured ||= YAML.load_file('packwerk.yml')['architecture_layers'].nil? 28 | end 29 | 30 | sig { returns(String) } 31 | def layers_key 32 | layers_key_configured? ? 'layers' : 'architecture_layers' 33 | end 34 | 35 | sig { returns(String) } 36 | def violation_key 37 | layers_key_configured? ? LAYER_VIOLATION_TYPE : ARCHITECTURE_VIOLATION_TYPE 38 | end 39 | 40 | sig { returns(String) } 41 | def enforce_key 42 | layers_key_configured? ? LAYER_ENFORCE : ARCHITECTURE_ENFORCE 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/packwerk/layer/layers.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | module Packwerk 5 | module Layer 6 | class Layers 7 | extend T::Sig 8 | 9 | sig { void } 10 | def initialize 11 | @names = T.let(@names, T.nilable(T::Set[String])) 12 | @names_list = T.let(@names_list, T.nilable(T::Array[String])) 13 | end 14 | 15 | sig { params(layer: String).returns(Integer) } 16 | def index_of(layer) 17 | index = names_list.reverse.find_index(layer) 18 | if index.nil? 19 | raise "Layer #{layer} not find, please run `bin/packwerk validate`" 20 | end 21 | 22 | index 23 | end 24 | 25 | sig { returns(T::Set[String]) } 26 | def names 27 | @names ||= Set.new(names_list) 28 | end 29 | 30 | sig { returns(T::Array[String]) } 31 | def names_list 32 | @names_list ||= Config.new.layers_list 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/packwerk/layer/package.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | module Packwerk 5 | module Layer 6 | class Package < T::Struct 7 | extend T::Sig 8 | 9 | const :layer, T.nilable(String) 10 | const :enforcement_setting, T.nilable(T.any(T::Boolean, String, T::Array[String])) 11 | const :config, T::Hash[T.untyped, T.untyped] 12 | 13 | sig { returns(T::Boolean) } 14 | def enforces? 15 | enforcement_setting == true || enforcement_setting == 'strict' 16 | end 17 | 18 | sig { params(other_package: Package, layers: Layers).returns(T::Boolean) } 19 | def can_depend_on?(other_package, layers:) 20 | return true if !enforces? 21 | 22 | flow_sensitive_layer = layer 23 | flow_sensitive_other_layer = other_package.layer 24 | return true if flow_sensitive_layer.nil? 25 | return true if flow_sensitive_other_layer.nil? 26 | 27 | layers.index_of(flow_sensitive_layer) >= layers.index_of(flow_sensitive_other_layer) 28 | end 29 | 30 | class << self 31 | extend T::Sig 32 | 33 | sig { params(package: ::Packwerk::Package, layers: Layers).returns(Package) } 34 | def from(package, layers) 35 | config = package.config 36 | 37 | # This allows the layer to be inferred based on the package root 38 | package_root = package.name.split('/').first 39 | if config['layer'] 40 | layer = config['layer'] 41 | elsif package_root && layers.names.include?(package_root) 42 | layer = package_root 43 | else 44 | layer = nil 45 | end 46 | 47 | Package.new( 48 | layer: layer, 49 | enforcement_setting: config[Config.new.enforce_key], 50 | config: config 51 | ) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/packwerk/layer/validator.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | module Packwerk 5 | module Layer 6 | class Validator 7 | extend T::Sig 8 | include Packwerk::Validator 9 | 10 | Result = Packwerk::Validator::Result 11 | 12 | sig { override.params(package_set: PackageSet, configuration: Configuration).returns(Result) } 13 | def call(package_set, configuration) 14 | results = T.let([], T::Array[Result]) 15 | 16 | package_set.each do |package| 17 | config = package.config 18 | f = Pathname.new(package.name).join('package.yml').to_s 19 | package = Package.from(package, layers) 20 | 21 | next if !config 22 | 23 | result = check_enforce_key(package, f, config) 24 | results << result 25 | next if !result.ok? 26 | 27 | result = check_enforce_layers_setting(f, config[layer_config.enforce_key]) 28 | results << result 29 | next if !result.ok? 30 | 31 | result = check_layer_setting(package, f) 32 | results << result 33 | next if !result.ok? 34 | end 35 | 36 | merge_results(results, separator: "\n---\n") 37 | end 38 | 39 | sig { returns(Layers) } 40 | def layers 41 | @layers ||= T.let(Layers.new, T.nilable(Packwerk::Layer::Layers)) 42 | end 43 | 44 | sig { returns(Config) } 45 | def layer_config 46 | @layer_config ||= T.let(Config.new, T.nilable(Config)) 47 | end 48 | 49 | sig { override.returns(T::Array[String]) } 50 | def permitted_keys 51 | [layer_config.enforce_key, 'layer'] 52 | end 53 | 54 | sig do 55 | params(package: Package, config_file_path: String, config: T::Hash[T.untyped, T.untyped]).returns(Result) 56 | end 57 | def check_enforce_key(package, config_file_path, config) 58 | enforce_layer_present = !config[Config::LAYER_ENFORCE].nil? 59 | enforce_architecture_present = !config[Config::ARCHITECTURE_ENFORCE].nil? 60 | 61 | if layer_config.enforce_key == Config::LAYER_ENFORCE && enforce_architecture_present 62 | Result.new( 63 | ok: false, 64 | error_value: "Unexpected `enforce_architecture` option in #{config_file_path.inspect}. Did you mean `enforce_layers`?" 65 | ) 66 | elsif layer_config.enforce_key == Config::ARCHITECTURE_ENFORCE && enforce_layer_present 67 | Result.new( 68 | ok: false, 69 | error_value: "Unexpected `enforce_layers` option in #{config_file_path.inspect}. Did you mean `enforce_architecture`?" 70 | ) 71 | else 72 | Result.new(ok: true) 73 | end 74 | end 75 | 76 | sig do 77 | params(package: Package, config_file_path: String).returns(Result) 78 | end 79 | def check_layer_setting(package, config_file_path) 80 | layer = package.layer 81 | valid_layer = layer.nil? || layers.names.include?(layer) 82 | 83 | if layer.nil? && package.enforces? 84 | Result.new( 85 | ok: false, 86 | error_value: "Invalid 'layer' option in #{config_file_path.inspect}: #{package.layer.inspect}. `layer` must be set if `#{layer_config.enforce_key}` is on." 87 | ) 88 | elsif valid_layer 89 | Result.new(ok: true) 90 | else 91 | Result.new( 92 | ok: false, 93 | error_value: "Invalid 'layer' option in #{config_file_path.inspect}: #{layer.inspect}. Must be one of #{layers.names_list.inspect}" 94 | ) 95 | end 96 | end 97 | 98 | sig do 99 | params(config_file_path: String, setting: T.untyped).returns(Result) 100 | end 101 | def check_enforce_layers_setting(config_file_path, setting) 102 | activated_value = [true, 'strict'].include?(setting) 103 | valid_value = [true, nil, false, 'strict'].include?(setting) 104 | layers_set = layers.names.any? 105 | if !valid_value 106 | Result.new( 107 | ok: false, 108 | error_value: "Invalid '#{layer_config.enforce_key}' option in #{config_file_path.inspect}: #{setting.inspect}" 109 | ) 110 | elsif activated_value && !layers_set 111 | Result.new( 112 | ok: false, 113 | error_value: "Cannot set '#{layer_config.enforce_key}' option in #{config_file_path.inspect} until `layers` have been specified in `packwerk.yml`" 114 | ) 115 | else 116 | Result.new(ok: true) 117 | end 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/packwerk/privacy/checker.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | require 'packwerk/privacy/package' 5 | require 'packwerk/privacy/validator' 6 | 7 | module Packwerk 8 | module Privacy 9 | # Checks whether a given reference references a private constant of another package. 10 | class Checker 11 | extend T::Sig 12 | include Packwerk::Checker 13 | 14 | VIOLATION_TYPE = T.let('privacy', String) 15 | PUBLICIZED_SIGIL = T.let('pack_public: true', String) 16 | PUBLICIZED_SIGIL_REGEX = T.let(/#.*pack_public:\s*true/, Regexp) 17 | @publicized_locations = T.let({}, T::Hash[String, T::Boolean]) 18 | 19 | class << self 20 | extend T::Sig 21 | 22 | sig { returns(T::Hash[String, T::Boolean]) } 23 | def publicized_locations 24 | @publicized_locations 25 | end 26 | 27 | sig { params(location: String).returns(T::Boolean) } 28 | def publicized_location?(location) 29 | unless publicized_locations.key?(location) 30 | publicized_locations[location] = check_for_publicized_sigil(location) 31 | end 32 | 33 | T.must(publicized_locations[location]) 34 | end 35 | 36 | sig { params(location: String).returns(T::Boolean) } 37 | def check_for_publicized_sigil(location) 38 | content_contains_sigil?(File.readlines(location)) 39 | end 40 | 41 | sig { params(lines: T::Array[String]).returns(T::Boolean) } 42 | def content_contains_sigil?(lines) 43 | T.must(lines[0..4]).any? { |l| l =~ PUBLICIZED_SIGIL_REGEX } 44 | end 45 | end 46 | 47 | sig { override.returns(String) } 48 | def violation_type 49 | VIOLATION_TYPE 50 | end 51 | 52 | sig do 53 | override 54 | .params(reference: Packwerk::Reference) 55 | .returns(T::Boolean) 56 | end 57 | def invalid_reference?(reference) 58 | constant_package = reference.constant.package 59 | privacy_package = Package.from(constant_package) 60 | 61 | return false if privacy_package.public_path?(reference.constant.location) 62 | return false if self.class.publicized_location?(reference.constant.location) 63 | 64 | privacy_option = privacy_package.enforce_privacy 65 | return false if enforcement_disabled?(privacy_option) 66 | 67 | return false if privacy_package.ignored_private_constants.include?(reference.constant.name) 68 | 69 | explicitly_private_constant?(reference.constant, explicitly_private_constants: privacy_package.private_constants) 70 | end 71 | 72 | sig do 73 | override 74 | .params(listed_offense: Packwerk::ReferenceOffense) 75 | .returns(T::Boolean) 76 | end 77 | def strict_mode_violation?(listed_offense) 78 | publishing_package = listed_offense.reference.constant.package 79 | 80 | return false unless publishing_package.config['enforce_privacy'] == 'strict' 81 | return false if exclude_from_strict?( 82 | publishing_package.config['strict_privacy_ignored_patterns'] || [], 83 | Pathname.new(listed_offense.reference.relative_path).cleanpath 84 | ) 85 | 86 | true 87 | end 88 | 89 | sig do 90 | override 91 | .params(reference: Packwerk::Reference) 92 | .returns(String) 93 | end 94 | def message(reference) 95 | source_desc = "'#{reference.package}'" 96 | 97 | message = <<~MESSAGE 98 | Privacy violation: '#{reference.constant.name}' is private to '#{reference.constant.package}' but referenced from #{source_desc}. 99 | Is there a public entrypoint in '#{Package.from(reference.constant.package).public_path}' that you can use instead? 100 | 101 | #{standard_help_message(reference)} 102 | MESSAGE 103 | 104 | message.chomp 105 | end 106 | 107 | private 108 | 109 | sig do 110 | params( 111 | constant: ConstantContext, 112 | explicitly_private_constants: T::Array[String] 113 | ).returns(T::Boolean) 114 | end 115 | def explicitly_private_constant?(constant, explicitly_private_constants:) 116 | return true if explicitly_private_constants.empty? 117 | 118 | explicitly_private_constants.include?(constant.name) || 119 | # nested constants 120 | explicitly_private_constants.any? { |epc| constant.name.start_with?("#{epc}::") } 121 | end 122 | 123 | sig do 124 | params(privacy_option: T.nilable(T.any(T::Boolean, String, T::Array[String]))) 125 | .returns(T::Boolean) 126 | end 127 | def enforcement_disabled?(privacy_option) 128 | [false, nil].include?(privacy_option) 129 | end 130 | 131 | sig { params(reference: Reference).returns(String) } 132 | def standard_help_message(reference) 133 | standard_message = <<~MESSAGE.chomp 134 | Inference details: this is a reference to #{reference.constant.name} which seems to be defined in #{reference.constant.location}. 135 | To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations 136 | MESSAGE 137 | 138 | standard_message.chomp 139 | end 140 | 141 | sig { params(globs: T::Array[String], path: Pathname).returns(T::Boolean) } 142 | def exclude_from_strict?(globs, path) 143 | globs.any? do |glob| 144 | path.fnmatch(glob, File::FNM_EXTGLOB) 145 | end 146 | end 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/packwerk/privacy/package.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | module Packwerk 5 | module Privacy 6 | class Package < T::Struct 7 | extend T::Sig 8 | 9 | const :public_path, String 10 | const :user_defined_public_path, T.nilable(String) 11 | const :enforce_privacy, T.nilable(T.any(T::Boolean, String)) 12 | const :private_constants, T::Array[String] 13 | const :ignored_private_constants, T::Array[String] 14 | const :strict_privacy_ignored_patterns, T::Array[String] 15 | 16 | sig { params(path: String).returns(T::Boolean) } 17 | def public_path?(path) 18 | path.start_with?(public_path) 19 | end 20 | 21 | class << self 22 | extend T::Sig 23 | 24 | sig { params(package: ::Packwerk::Package).returns(Package) } 25 | def from(package) 26 | Package.new( 27 | public_path: public_path_for(package), 28 | user_defined_public_path: user_defined_public_path(package), 29 | enforce_privacy: package.config['enforce_privacy'], 30 | private_constants: package.config['private_constants'] || [], 31 | ignored_private_constants: package.config['ignored_private_constants'] || [], 32 | strict_privacy_ignored_patterns: package.config['strict_privacy_ignored_patterns'] || [] 33 | ) 34 | end 35 | 36 | sig { params(package: ::Packwerk::Package).returns(T.nilable(String)) } 37 | def user_defined_public_path(package) 38 | return unless package.config['public_path'] 39 | return package.config['public_path'] if package.config['public_path'].end_with?('/') 40 | 41 | "#{package.config['public_path']}/" 42 | end 43 | 44 | sig { params(package: ::Packwerk::Package).returns(String) } 45 | def public_path_for(package) 46 | unprefixed_public_path = user_defined_public_path(package) || 'app/public/' 47 | 48 | if package.root? 49 | unprefixed_public_path 50 | else 51 | File.join(package.name, unprefixed_public_path) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/packwerk/privacy/validator.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | module Packwerk 5 | module Privacy 6 | class Validator 7 | extend T::Sig 8 | include Packwerk::Validator 9 | 10 | Result = Packwerk::Validator::Result 11 | 12 | sig { override.params(package_set: PackageSet, configuration: Configuration).returns(Result) } 13 | def call(package_set, configuration) 14 | privacy_settings = package_manifests_settings_for(configuration, 'enforce_privacy') 15 | 16 | results = T.let([], T::Array[Result]) 17 | 18 | privacy_settings.each do |config_file_path, setting| 19 | results << check_enforce_privacy_setting(config_file_path, setting) 20 | end 21 | 22 | results += verify_private_constants_setting(package_set, configuration) 23 | 24 | public_path_settings = package_manifests_settings_for(configuration, 'public_path') 25 | public_path_settings.each do |config_file_path, setting| 26 | results << check_public_path(config_file_path, setting) 27 | end 28 | 29 | merge_results(results, separator: "\n---\n") 30 | end 31 | 32 | sig { override.returns(T::Array[String]) } 33 | def permitted_keys 34 | %w[public_path enforce_privacy private_constants ignored_private_constants strict_privacy_ignored_patterns] 35 | end 36 | 37 | private 38 | 39 | sig { params(package_set: PackageSet, configuration: Configuration).returns(T::Array[Result]) } 40 | def verify_private_constants_setting(package_set, configuration) 41 | private_constants_setting = package_manifests_settings_for(configuration, 'private_constants') 42 | results = T.let([], T::Array[Result]) 43 | resolver = ConstantResolver.new( 44 | root_path: configuration.root_path, 45 | load_paths: configuration.load_paths, 46 | inflector: ActiveSupport::Inflector 47 | ) 48 | 49 | private_constants_setting.each do |config_file_path, setting| 50 | next if setting.nil? 51 | 52 | unless setting.is_a?(Array) 53 | results << Result.new( 54 | ok: false, 55 | error_value: "Invalid 'private_constants' setting: #{setting.inspect}" 56 | ) 57 | next 58 | end 59 | 60 | constants = setting 61 | 62 | results += assert_constants_can_be_loaded(constants, config_file_path) 63 | 64 | constant_locations = constants.map { |c| [c, resolver.resolve(c)&.location] } 65 | 66 | constant_locations.each do |name, location| 67 | results << if location 68 | check_private_constant_location(configuration, package_set, name, location, config_file_path) 69 | else 70 | private_constant_unresolvable(name, config_file_path) 71 | end 72 | end 73 | end 74 | 75 | results 76 | end 77 | 78 | sig do 79 | params(config_file_path: String, setting: T.untyped).returns(Result) 80 | end 81 | def check_public_path(config_file_path, setting) 82 | if setting.is_a?(String) || setting.nil? 83 | Result.new(ok: true) 84 | else 85 | Result.new( 86 | ok: false, 87 | error_value: "'public_path' option must be a string in #{config_file_path.inspect}: #{setting.inspect}" 88 | ) 89 | end 90 | end 91 | 92 | sig do 93 | params(config_file_path: String, setting: T.untyped).returns(Result) 94 | end 95 | def check_enforce_privacy_setting(config_file_path, setting) 96 | if [TrueClass, FalseClass, NilClass].include?(setting.class) || setting == 'strict' 97 | Result.new(ok: true) 98 | else 99 | Result.new( 100 | ok: false, 101 | error_value: "Invalid 'enforce_privacy' option in #{config_file_path.inspect}: #{setting.inspect}" 102 | ) 103 | end 104 | end 105 | 106 | sig do 107 | params(configuration: Configuration, package_set: PackageSet, name: T.untyped, location: T.untyped, 108 | config_file_path: T.untyped).returns(Result) 109 | end 110 | def check_private_constant_location(configuration, package_set, name, location, config_file_path) 111 | declared_package = package_set.package_from_path(relative_path(configuration, config_file_path)) 112 | constant_package = package_set.package_from_path(location) 113 | if constant_package == declared_package 114 | check_for_publicized_constant(location, constant_package, name) 115 | else 116 | Result.new( 117 | ok: false, 118 | error_value: "'#{name}' is declared as private in the '#{declared_package}' package but appears to be " \ 119 | "defined\nin the '#{constant_package}' package. Packwerk resolved it to #{location}." 120 | ) 121 | end 122 | end 123 | 124 | sig { params(location: String, constant_package: Packwerk::Package, name: T.untyped).returns(Result) } 125 | def check_for_publicized_constant(location, constant_package, name) 126 | if Packwerk::Privacy::Checker.publicized_location?(location) 127 | sigil = Packwerk::Privacy::Checker::PUBLICIZED_SIGIL 128 | Result.new( 129 | ok: false, 130 | error_value: "'#{name}' is an explicitly publicized constant declared in #{location} through usage of " \ 131 | "'#{sigil}'. However, the package '#{constant_package}' is also declaring it as a private " \ 132 | "constant. This conflict must be resolved. Either remove '#{sigil}' from #{location} or " \ 133 | 'remove this constant from the list of private constants in the config for ' \ 134 | "'#{constant_package}'." 135 | ) 136 | else 137 | Result.new(ok: true) 138 | end 139 | end 140 | 141 | sig { params(constants: T.untyped, config_file_path: String).returns(T::Array[Result]) } 142 | def assert_constants_can_be_loaded(constants, config_file_path) 143 | constants.map do |constant| 144 | if constant.start_with?('::') 145 | constant.try(&:constantize) && Result.new(ok: true) 146 | else 147 | error_value = "'#{constant}', listed in the 'private_constants' option " \ 148 | "in #{config_file_path}, is invalid.\nPrivate constants need to be " \ 149 | 'prefixed with the top-level namespace operator `::`.' 150 | Result.new( 151 | ok: false, 152 | error_value: error_value 153 | ) 154 | end 155 | end 156 | end 157 | 158 | sig { params(name: T.untyped, config_file_path: T.untyped).returns(Result) } 159 | def private_constant_unresolvable(name, config_file_path) 160 | explicit_filepath = "#{(name.start_with?('::') ? name[2..] : name).underscore}.rb" 161 | 162 | Result.new( 163 | ok: false, 164 | error_value: "'#{name}', listed in #{config_file_path}, could not be resolved.\n" \ 165 | "This is probably because it is an autovivified namespace - a namespace module that doesn't have a\n" \ 166 | "file explicitly defining it. Packwerk currently doesn't support declaring autovivified namespaces as\n" \ 167 | "private. Add a #{explicit_filepath} file to explicitly define the constant." 168 | ) 169 | end 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /lib/packwerk/visibility/checker.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | require 'packwerk/visibility/package' 5 | require 'packwerk/visibility/validator' 6 | 7 | module Packwerk 8 | module Visibility 9 | # Checks whether a given reference references a constant from a package that does not permit visibility 10 | class Checker 11 | extend T::Sig 12 | include Packwerk::Checker 13 | 14 | VIOLATION_TYPE = T.let('visibility', String) 15 | 16 | sig { override.returns(String) } 17 | def violation_type 18 | VIOLATION_TYPE 19 | end 20 | 21 | sig do 22 | override 23 | .params(reference: Packwerk::Reference) 24 | .returns(T::Boolean) 25 | end 26 | def invalid_reference?(reference) 27 | constant_package = reference.constant.package 28 | visibility_package = Package.from(constant_package) 29 | visibility_option = visibility_package.enforce_visibility 30 | return false if enforcement_disabled?(visibility_option) 31 | 32 | !visibility_package.visible_to.include?(reference.package.name) 33 | end 34 | 35 | sig do 36 | override 37 | .params(listed_offense: Packwerk::ReferenceOffense) 38 | .returns(T::Boolean) 39 | end 40 | def strict_mode_violation?(listed_offense) 41 | publishing_package = listed_offense.reference.constant.package 42 | publishing_package.config['enforce_visibility'] == 'strict' 43 | end 44 | 45 | sig do 46 | override 47 | .params(reference: Packwerk::Reference) 48 | .returns(String) 49 | end 50 | def message(reference) 51 | source_desc = "'#{reference.package}'" 52 | 53 | message = <<~MESSAGE 54 | Visibility violation: '#{reference.constant.name}' belongs to '#{reference.constant.package}', which is not visible to #{source_desc}. 55 | Is there a different package to use instead, or should '#{reference.constant.package}' also be visible to #{source_desc}? 56 | 57 | #{standard_help_message(reference)} 58 | MESSAGE 59 | 60 | message.chomp 61 | end 62 | 63 | private 64 | 65 | sig do 66 | params(visibility_option: T.nilable(T.any(T::Boolean, String))) 67 | .returns(T::Boolean) 68 | end 69 | def enforcement_disabled?(visibility_option) 70 | [false, nil].include?(visibility_option) 71 | end 72 | 73 | sig { params(reference: Reference).returns(String) } 74 | def standard_help_message(reference) 75 | standard_message = <<~MESSAGE.chomp 76 | Inference details: this is a reference to #{reference.constant.name} which seems to be defined in #{reference.constant.location}. 77 | To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations 78 | MESSAGE 79 | 80 | standard_message.chomp 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/packwerk/visibility/package.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | module Packwerk 5 | module Visibility 6 | class Package < T::Struct 7 | extend T::Sig 8 | 9 | const :visible_to, T::Array[String] 10 | const :enforce_visibility, T.nilable(T.any(T::Boolean, String)) 11 | 12 | class << self 13 | extend T::Sig 14 | 15 | sig { params(package: ::Packwerk::Package).returns(Package) } 16 | def from(package) 17 | Package.new( 18 | visible_to: package.config['visible_to'] || [], 19 | enforce_visibility: package.config['enforce_visibility'] 20 | ) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/packwerk/visibility/validator.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | # frozen_string_literal: true 3 | 4 | module Packwerk 5 | module Visibility 6 | class Validator 7 | extend T::Sig 8 | include Packwerk::Validator 9 | 10 | Result = Packwerk::Validator::Result 11 | 12 | sig { override.params(package_set: PackageSet, configuration: Configuration).returns(Result) } 13 | def call(package_set, configuration) 14 | visible_settings = package_manifests_settings_for(configuration, 'visible_to') 15 | results = T.let([], T::Array[Result]) 16 | 17 | all_package_names = package_set.to_set(&:name) 18 | 19 | package_manifests_settings_for(configuration, 'enforce_visibility').each do |config, setting| 20 | next if setting.nil? 21 | 22 | next if [TrueClass, FalseClass].include?(setting.class) || setting == 'strict' 23 | 24 | results << Result.new( 25 | ok: false, 26 | error_value: "\tInvalid 'enforce_visibility' option: #{setting.inspect} in #{config.inspect}" 27 | ) 28 | end 29 | 30 | visible_settings.each do |config_file_path, setting| 31 | next if setting.nil? 32 | 33 | if setting.is_a?(Array) 34 | packages_not_found = setting.to_set - all_package_names 35 | 36 | if packages_not_found.any? 37 | results << Result.new( 38 | ok: false, 39 | error_value: "'visible_to' option must only contain valid packages in #{config_file_path.inspect}. Invalid packages: #{packages_not_found.to_a.inspect}" 40 | ) 41 | end 42 | else 43 | results << Result.new( 44 | ok: false, 45 | error_value: "'visible_to' option must be an array in #{config_file_path.inspect}." 46 | ) 47 | end 48 | end 49 | 50 | merge_results(results, separator: "\n---\n") 51 | end 52 | 53 | sig { override.returns(T::Array[String]) } 54 | def permitted_keys 55 | %w[visible_to enforce_visibility] 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /packwerk-extensions.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = 'packwerk-extensions' 3 | spec.version = '0.3.0' 4 | spec.authors = ['Gusto Engineers'] 5 | spec.email = ['dev@gusto.com'] 6 | 7 | spec.summary = 'A collection of extensions for packwerk packages.' 8 | spec.description = 'A collection of extensions for packwerk packages.' 9 | spec.homepage = 'https://github.com/rubyatscale/packwerk-extensions' 10 | spec.license = 'MIT' 11 | 12 | if spec.respond_to?(:metadata) 13 | spec.metadata['homepage_uri'] = spec.homepage 14 | spec.metadata['source_code_uri'] = 'https://github.com/rubyatscale/packwerk-extensions' 15 | spec.metadata['changelog_uri'] = 'https://github.com/rubyatscale/packwerk-extensions/releases' 16 | spec.metadata['allowed_push_host'] = 'https://rubygems.org' 17 | else 18 | raise 'RubyGems 2.0 or newer is required to protect against ' \ 19 | 'public gem pushes.' 20 | end 21 | 22 | spec.required_ruby_version = Gem::Requirement.new('>= 2.7') 23 | # Specify which files should be added to the gem when it is released. 24 | spec.files = Dir['README.md', 'lib/**/*'] 25 | 26 | spec.add_dependency 'packwerk', '>= 2.2.1' 27 | spec.add_dependency 'railties', '>= 6.0.0' 28 | spec.add_dependency 'sorbet-runtime' 29 | spec.add_dependency 'zeitwerk' 30 | 31 | spec.add_development_dependency 'minitest' 32 | spec.add_development_dependency 'mocha' 33 | spec.add_development_dependency 'pry' 34 | spec.add_development_dependency 'rake' 35 | spec.add_development_dependency 'rubocop' 36 | spec.add_development_dependency 'sorbet' 37 | spec.add_development_dependency 'sorbet-static' 38 | spec.add_development_dependency 'tapioca' 39 | end 40 | -------------------------------------------------------------------------------- /sorbet/config: -------------------------------------------------------------------------------- 1 | --dir 2 | . 3 | --enable-experimental-requires-ancestor 4 | --ignore=vendor/bundle 5 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/actioncable@7.0.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `actioncable` gem. 5 | # Please instead update this file by running `bin/tapioca gem actioncable`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/actionmailbox@7.0.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `actionmailbox` gem. 5 | # Please instead update this file by running `bin/tapioca gem actionmailbox`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/actionmailer@7.0.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `actionmailer` gem. 5 | # Please instead update this file by running `bin/tapioca gem actionmailer`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/actiontext@7.0.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `actiontext` gem. 5 | # Please instead update this file by running `bin/tapioca gem actiontext`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/activejob@7.0.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `activejob` gem. 5 | # Please instead update this file by running `bin/tapioca gem activejob`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/activemodel@7.0.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `activemodel` gem. 5 | # Please instead update this file by running `bin/tapioca gem activemodel`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/activerecord@7.0.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `activerecord` gem. 5 | # Please instead update this file by running `bin/tapioca gem activerecord`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/activestorage@7.0.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `activestorage` gem. 5 | # Please instead update this file by running `bin/tapioca gem activestorage`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/builder@3.2.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `builder` gem. 5 | # Please instead update this file by running `bin/tapioca gem builder`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/constant_resolver@0.2.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `constant_resolver` gem. 5 | # Please instead update this file by running `bin/tapioca gem constant_resolver`. 6 | 7 | # Get information about (partially qualified) constants without loading the application code. 8 | # We infer the fully qualified name and the filepath. 9 | # 10 | # The implementation makes a few assumptions about the code base: 11 | # - `Something::SomeOtherThing` is defined in a path of either `something/some_other_thing.rb` or `something.rb`, 12 | # relative to the load path. Constants that have their own file do not have all-uppercase names like MAGIC_NUMBER or 13 | # all-uppercase parts like SomeID. 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 | # 18 | # source://constant_resolver//lib/constant_resolver/version.rb#3 19 | class ConstantResolver 20 | # @example usage in a Rails app 21 | # config = Rails.application.config 22 | # load_paths = (config.eager_load_paths + config.autoload_paths + config.autoload_once_paths) 23 | # .map { |p| Pathname.new(p).relative_path_from(Rails.root).to_s } 24 | # ConstantResolver.new( 25 | # root_path: Rails.root.to_s, 26 | # load_paths: load_paths 27 | # ) 28 | # @param root_path [String] The root path of the application to analyze 29 | # @param load_paths [Array] The autoload paths of the application. 30 | # @param inflector [Object] Any object that implements a `camelize` function. 31 | # @return [ConstantResolver] a new instance of ConstantResolver 32 | # 33 | # source://constant_resolver//lib/constant_resolver.rb#42 34 | def initialize(root_path:, load_paths:, inflector: T.unsafe(nil)); end 35 | 36 | # @api private 37 | # 38 | # source://constant_resolver//lib/constant_resolver.rb#113 39 | def config; end 40 | 41 | # Maps constant names to file paths. 42 | # 43 | # @return [Hash] 44 | # 45 | # source://constant_resolver//lib/constant_resolver.rb#73 46 | def file_map; end 47 | 48 | # Resolve a constant via its name. 49 | # If the name is partially qualified, we need the current namespace path to correctly infer its full name 50 | # 51 | # @param const_name [String] The constant's name, fully or partially qualified. 52 | # @param current_namespace_path [Array] (optional) The namespace of the context in which the constant is 53 | # used, e.g. ["Apps", "Models"] for `Apps::Models`. Defaults to [] which means top level. 54 | # @return [ConstantResolver::ConstantContext] 55 | # 56 | # source://constant_resolver//lib/constant_resolver.rb#58 57 | def resolve(const_name, current_namespace_path: T.unsafe(nil)); end 58 | 59 | private 60 | 61 | # source://constant_resolver//lib/constant_resolver.rb#131 62 | def ambiguous_constant_message(const_name, paths); end 63 | 64 | # source://constant_resolver//lib/constant_resolver.rb#122 65 | def coerce_load_paths(load_paths); end 66 | 67 | # source://constant_resolver//lib/constant_resolver.rb#138 68 | def glob_path(path); end 69 | 70 | # source://constant_resolver//lib/constant_resolver.rb#142 71 | def resolve_constant(const_name, current_namespace_path, original_name: T.unsafe(nil)); end 72 | 73 | # source://constant_resolver//lib/constant_resolver.rb#155 74 | def resolve_traversing_namespace_path(const_name, current_namespace_path); end 75 | end 76 | 77 | # source://constant_resolver//lib/constant_resolver.rb#17 78 | class ConstantResolver::ConstantContext < ::Struct; end 79 | 80 | # source://constant_resolver//lib/constant_resolver.rb#19 81 | class ConstantResolver::DefaultInflector 82 | # source://constant_resolver//lib/constant_resolver.rb#20 83 | def camelize(string); end 84 | end 85 | 86 | # source://constant_resolver//lib/constant_resolver.rb#16 87 | class ConstantResolver::Error < ::StandardError; end 88 | 89 | # source://constant_resolver//lib/constant_resolver/version.rb#4 90 | ConstantResolver::VERSION = T.let(T.unsafe(nil), String) 91 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/diff-lcs@1.5.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `diff-lcs` gem. 5 | # Please instead update this file by running `bin/tapioca gem diff-lcs`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/erubi@1.11.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `erubi` gem. 5 | # Please instead update this file by running `bin/tapioca gem erubi`. 6 | 7 | # source://erubi//lib/erubi.rb#3 8 | module Erubi 9 | class << self 10 | # source://erubi//lib/erubi.rb#28 11 | def h(value); end 12 | end 13 | end 14 | 15 | # source://erubi//lib/erubi.rb#46 16 | class Erubi::Engine 17 | # Initialize a new Erubi::Engine. Options: 18 | # +:bufval+ :: The value to use for the buffer variable, as a string (default '::String.new'). 19 | # +:bufvar+ :: The variable name to use for the buffer variable, as a string. 20 | # +:chain_appends+ :: Whether to chain << calls to the buffer variable. Offers better 21 | # performance, but can cause issues when the buffer variable is reassigned during 22 | # template rendering (default +false+). 23 | # +:ensure+ :: Wrap the template in a begin/ensure block restoring the previous value of bufvar. 24 | # +:escapefunc+ :: The function to use for escaping, as a string (default: '::Erubi.h'). 25 | # +:escape+ :: Whether to make <%= escape by default, and <%== not escape by default. 26 | # +:escape_html+ :: Same as +:escape+, with lower priority. 27 | # +:filename+ :: The filename for the template. 28 | # the resulting source code. Note this may cause problems if you are wrapping the resulting 29 | # source code in other code, because the magic comment only has an effect at the beginning of 30 | # the file, and having the magic comment later in the file can trigger warnings. 31 | # +:freeze_template_literals+ :: Whether to suffix all literal strings for template code with .freeze 32 | # (default: +true+ on Ruby 2.1+, +false+ on Ruby 2.0 and older). 33 | # Can be set to +false+ on Ruby 2.3+ when frozen string literals are enabled 34 | # in order to improve performance. 35 | # +:literal_prefix+ :: The prefix to output when using escaped tag delimiters (default '<%'). 36 | # +:literal_postfix+ :: The postfix to output when using escaped tag delimiters (default '%>'). 37 | # +:outvar+ :: Same as +:bufvar+, with lower priority. 38 | # +:postamble+ :: The postamble for the template, by default returns the resulting source code. 39 | # +:preamble+ :: The preamble for the template, by default initializes the buffer variable. 40 | # +:regexp+ :: The regexp to use for scanning. 41 | # +:src+ :: The initial value to use for the source code, an empty string by default. 42 | # +:trim+ :: Whether to trim leading and trailing whitespace, true by default. 43 | # 44 | # @return [Engine] a new instance of Engine 45 | # 46 | # source://erubi//lib/erubi.rb#86 47 | def initialize(input, properties = T.unsafe(nil)); end 48 | 49 | # The variable name used for the buffer variable. 50 | # 51 | # source://erubi//lib/erubi.rb#57 52 | def bufvar; end 53 | 54 | # The filename of the template, if one was given. 55 | # 56 | # source://erubi//lib/erubi.rb#54 57 | def filename; end 58 | 59 | # The frozen ruby source code generated from the template, which can be evaled. 60 | # 61 | # source://erubi//lib/erubi.rb#51 62 | def src; end 63 | 64 | private 65 | 66 | # Add ruby code to the template 67 | # 68 | # source://erubi//lib/erubi.rb#218 69 | def add_code(code); end 70 | 71 | # Add the given ruby expression result to the template, 72 | # escaping it based on the indicator given and escape flag. 73 | # 74 | # source://erubi//lib/erubi.rb#227 75 | def add_expression(indicator, code); end 76 | 77 | # Add the result of Ruby expression to the template 78 | # 79 | # source://erubi//lib/erubi.rb#236 80 | def add_expression_result(code); end 81 | 82 | # Add the escaped result of Ruby expression to the template 83 | # 84 | # source://erubi//lib/erubi.rb#241 85 | def add_expression_result_escaped(code); end 86 | 87 | # Add the given postamble to the src. Can be overridden in subclasses 88 | # to make additional changes to src that depend on the current state. 89 | # 90 | # source://erubi//lib/erubi.rb#247 91 | def add_postamble(postamble); end 92 | 93 | # Add raw text to the template. Modifies argument if argument is mutable as a memory optimization. 94 | # Must be called with a string, cannot be called with nil (Rails's subclass depends on it). 95 | # 96 | # source://erubi//lib/erubi.rb#205 97 | def add_text(text); end 98 | 99 | # Raise an exception, as the base engine class does not support handling other indicators. 100 | # 101 | # @raise [ArgumentError] 102 | # 103 | # source://erubi//lib/erubi.rb#253 104 | def handle(indicator, code, tailch, rspace, lspace); end 105 | 106 | # Make sure that any current expression has been terminated. 107 | # The default is to terminate all expressions, but when 108 | # the chain_appends option is used, expressions may not be 109 | # terminated. 110 | # 111 | # source://erubi//lib/erubi.rb#281 112 | def terminate_expression; end 113 | 114 | # Make sure the buffer variable is the target of the next append 115 | # before yielding to the block. Mark that the buffer is the target 116 | # of the next append after the block executes. 117 | # 118 | # This method should only be called if the block will result in 119 | # code where << will append to the bufvar. 120 | # 121 | # source://erubi//lib/erubi.rb#263 122 | def with_buffer; end 123 | end 124 | 125 | # The default regular expression used for scanning. 126 | # 127 | # source://erubi//lib/erubi.rb#48 128 | Erubi::Engine::DEFAULT_REGEXP = T.let(T.unsafe(nil), Regexp) 129 | 130 | # source://erubi//lib/erubi.rb#15 131 | Erubi::MATCH_METHOD = T.let(T.unsafe(nil), Symbol) 132 | 133 | # source://erubi//lib/erubi.rb#8 134 | Erubi::RANGE_FIRST = T.let(T.unsafe(nil), Integer) 135 | 136 | # source://erubi//lib/erubi.rb#9 137 | Erubi::RANGE_LAST = T.let(T.unsafe(nil), Integer) 138 | 139 | # source://erubi//lib/erubi.rb#16 140 | Erubi::SKIP_DEFINED_FOR_INSTANCE_VARIABLE = T.let(T.unsafe(nil), FalseClass) 141 | 142 | # source://erubi//lib/erubi.rb#4 143 | Erubi::VERSION = T.let(T.unsafe(nil), String) 144 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/globalid@1.0.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `globalid` gem. 5 | # Please instead update this file by running `bin/tapioca gem globalid`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/mail@2.7.1.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `mail` gem. 5 | # Please instead update this file by running `bin/tapioca gem mail`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/marcel@1.0.2.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `marcel` gem. 5 | # Please instead update this file by running `bin/tapioca gem marcel`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/method_source@1.0.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `method_source` gem. 5 | # Please instead update this file by running `bin/tapioca gem method_source`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/mini_mime@1.1.2.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `mini_mime` gem. 5 | # Please instead update this file by running `bin/tapioca gem mini_mime`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/net-imap@0.3.1.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `net-imap` gem. 5 | # Please instead update this file by running `bin/tapioca gem net-imap`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/net-pop@0.1.2.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `net-pop` gem. 5 | # Please instead update this file by running `bin/tapioca gem net-pop`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/net-protocol@0.1.3.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `net-protocol` gem. 5 | # Please instead update this file by running `bin/tapioca gem net-protocol`. 6 | 7 | Net::NetPrivate::Socket = Net::InternetMessageIO 8 | Net::ProtocRetryError = Net::ProtoRetriableError 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/net-smtp@0.3.3.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `net-smtp` gem. 5 | # Please instead update this file by running `bin/tapioca gem net-smtp`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/netrc@0.11.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `netrc` gem. 5 | # Please instead update this file by running `bin/tapioca gem netrc`. 6 | 7 | # source://netrc//lib/netrc.rb#3 8 | class Netrc 9 | # @return [Netrc] a new instance of Netrc 10 | # 11 | # source://netrc//lib/netrc.rb#166 12 | def initialize(path, data); end 13 | 14 | # source://netrc//lib/netrc.rb#180 15 | def [](k); end 16 | 17 | # source://netrc//lib/netrc.rb#188 18 | def []=(k, info); end 19 | 20 | # source://netrc//lib/netrc.rb#200 21 | def delete(key); end 22 | 23 | # source://netrc//lib/netrc.rb#211 24 | def each(&block); end 25 | 26 | # source://netrc//lib/netrc.rb#196 27 | def length; end 28 | 29 | # source://netrc//lib/netrc.rb#215 30 | def new_item(m, l, p); end 31 | 32 | # Returns the value of attribute new_item_prefix. 33 | # 34 | # source://netrc//lib/netrc.rb#178 35 | def new_item_prefix; end 36 | 37 | # Sets the attribute new_item_prefix 38 | # 39 | # @param value the value to set the attribute new_item_prefix to. 40 | # 41 | # source://netrc//lib/netrc.rb#178 42 | def new_item_prefix=(_arg0); end 43 | 44 | # source://netrc//lib/netrc.rb#219 45 | def save; end 46 | 47 | # source://netrc//lib/netrc.rb#233 48 | def unparse; end 49 | 50 | class << self 51 | # source://netrc//lib/netrc.rb#42 52 | def check_permissions(path); end 53 | 54 | # source://netrc//lib/netrc.rb#33 55 | def config; end 56 | 57 | # @yield [self.config] 58 | # 59 | # source://netrc//lib/netrc.rb#37 60 | def configure; end 61 | 62 | # source://netrc//lib/netrc.rb#10 63 | def default_path; end 64 | 65 | # source://netrc//lib/netrc.rb#14 66 | def home_path; end 67 | 68 | # source://netrc//lib/netrc.rb#85 69 | def lex(lines); end 70 | 71 | # source://netrc//lib/netrc.rb#29 72 | def netrc_filename; end 73 | 74 | # Returns two values, a header and a list of items. 75 | # Each item is a tuple, containing some or all of: 76 | # - machine keyword (including trailing whitespace+comments) 77 | # - machine name 78 | # - login keyword (including surrounding whitespace+comments) 79 | # - login 80 | # - password keyword (including surrounding whitespace+comments) 81 | # - password 82 | # - trailing chars 83 | # This lets us change individual fields, then write out the file 84 | # with all its original formatting. 85 | # 86 | # source://netrc//lib/netrc.rb#129 87 | def parse(ts); end 88 | 89 | # Reads path and parses it as a .netrc file. If path doesn't 90 | # exist, returns an empty object. Decrypt paths ending in .gpg. 91 | # 92 | # source://netrc//lib/netrc.rb#51 93 | def read(path = T.unsafe(nil)); end 94 | 95 | # @return [Boolean] 96 | # 97 | # source://netrc//lib/netrc.rb#112 98 | def skip?(s); end 99 | end 100 | end 101 | 102 | # source://netrc//lib/netrc.rb#8 103 | Netrc::CYGWIN = T.let(T.unsafe(nil), T.untyped) 104 | 105 | # source://netrc//lib/netrc.rb#244 106 | class Netrc::Entry < ::Struct 107 | # Returns the value of attribute login 108 | # 109 | # @return [Object] the current value of login 110 | def login; end 111 | 112 | # Sets the attribute login 113 | # 114 | # @param value [Object] the value to set the attribute login to. 115 | # @return [Object] the newly set value 116 | # 117 | # source://netrc//lib/netrc.rb#244 118 | def login=(_); end 119 | 120 | # Returns the value of attribute password 121 | # 122 | # @return [Object] the current value of password 123 | def password; end 124 | 125 | # Sets the attribute password 126 | # 127 | # @param value [Object] the value to set the attribute password to. 128 | # @return [Object] the newly set value 129 | # 130 | # source://netrc//lib/netrc.rb#244 131 | def password=(_); end 132 | 133 | def to_ary; end 134 | 135 | class << self 136 | def [](*_arg0); end 137 | def inspect; end 138 | def members; end 139 | def new(*_arg0); end 140 | end 141 | end 142 | 143 | # source://netrc//lib/netrc.rb#250 144 | class Netrc::Error < ::StandardError; end 145 | 146 | # source://netrc//lib/netrc.rb#68 147 | class Netrc::TokenArray < ::Array 148 | # source://netrc//lib/netrc.rb#76 149 | def readto; end 150 | 151 | # source://netrc//lib/netrc.rb#69 152 | def take; end 153 | end 154 | 155 | # source://netrc//lib/netrc.rb#4 156 | Netrc::VERSION = T.let(T.unsafe(nil), String) 157 | 158 | # see http://stackoverflow.com/questions/4871309/what-is-the-correct-way-to-detect-if-ruby-is-running-on-windows 159 | # 160 | # source://netrc//lib/netrc.rb#7 161 | Netrc::WINDOWS = T.let(T.unsafe(nil), T.untyped) 162 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/nio4r@2.5.8.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `nio4r` gem. 5 | # Please instead update this file by running `bin/tapioca gem nio4r`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/parallel@1.22.1.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `parallel` gem. 5 | # Please instead update this file by running `bin/tapioca gem parallel`. 6 | 7 | # source://parallel//lib/parallel/version.rb#2 8 | module Parallel 9 | extend ::Parallel::ProcessorCount 10 | 11 | class << self 12 | # @return [Boolean] 13 | # 14 | # source://parallel//lib/parallel.rb#246 15 | def all?(*args, &block); end 16 | 17 | # @return [Boolean] 18 | # 19 | # source://parallel//lib/parallel.rb#241 20 | def any?(*args, &block); end 21 | 22 | # source://parallel//lib/parallel.rb#237 23 | def each(array, options = T.unsafe(nil), &block); end 24 | 25 | # source://parallel//lib/parallel.rb#251 26 | def each_with_index(array, options = T.unsafe(nil), &block); end 27 | 28 | # source://parallel//lib/parallel.rb#306 29 | def flat_map(*args, &block); end 30 | 31 | # source://parallel//lib/parallel.rb#231 32 | def in_processes(options = T.unsafe(nil), &block); end 33 | 34 | # source://parallel//lib/parallel.rb#215 35 | def in_threads(options = T.unsafe(nil)); end 36 | 37 | # source://parallel//lib/parallel.rb#255 38 | def map(source, options = T.unsafe(nil), &block); end 39 | 40 | # source://parallel//lib/parallel.rb#302 41 | def map_with_index(array, options = T.unsafe(nil), &block); end 42 | 43 | # source://parallel//lib/parallel.rb#310 44 | def worker_number; end 45 | 46 | # TODO: this does not work when doing threads in forks, so should remove and yield the number instead if needed 47 | # 48 | # source://parallel//lib/parallel.rb#315 49 | def worker_number=(worker_num); end 50 | 51 | private 52 | 53 | # source://parallel//lib/parallel.rb#321 54 | def add_progress_bar!(job_factory, options); end 55 | 56 | # source://parallel//lib/parallel.rb#584 57 | def call_with_index(item, index, options, &block); end 58 | 59 | # source://parallel//lib/parallel.rb#516 60 | def create_workers(job_factory, options, &block); end 61 | 62 | # options is either a Integer or a Hash with :count 63 | # 64 | # source://parallel//lib/parallel.rb#574 65 | def extract_count_from_options(options); end 66 | 67 | # source://parallel//lib/parallel.rb#602 68 | def instrument_finish(item, index, result, options); end 69 | 70 | # source://parallel//lib/parallel.rb#607 71 | def instrument_start(item, index, options); end 72 | 73 | # source://parallel//lib/parallel.rb#550 74 | def process_incoming_jobs(read, write, job_factory, options, &block); end 75 | 76 | # source://parallel//lib/parallel.rb#504 77 | def replace_worker(job_factory, workers, index, options, blk); end 78 | 79 | # source://parallel//lib/parallel.rb#595 80 | def with_instrumentation(item, index, options); end 81 | 82 | # source://parallel//lib/parallel.rb#346 83 | def work_direct(job_factory, options, &block); end 84 | 85 | # source://parallel//lib/parallel.rb#456 86 | def work_in_processes(job_factory, options, &blk); end 87 | 88 | # source://parallel//lib/parallel.rb#390 89 | def work_in_ractors(job_factory, options); end 90 | 91 | # source://parallel//lib/parallel.rb#365 92 | def work_in_threads(job_factory, options, &block); end 93 | 94 | # source://parallel//lib/parallel.rb#524 95 | def worker(job_factory, options, &block); end 96 | end 97 | end 98 | 99 | # source://parallel//lib/parallel.rb#14 100 | class Parallel::Break < ::StandardError 101 | # @return [Break] a new instance of Break 102 | # 103 | # source://parallel//lib/parallel.rb#17 104 | def initialize(value = T.unsafe(nil)); end 105 | 106 | # Returns the value of attribute value. 107 | # 108 | # source://parallel//lib/parallel.rb#15 109 | def value; end 110 | end 111 | 112 | # source://parallel//lib/parallel.rb#11 113 | class Parallel::DeadWorker < ::StandardError; end 114 | 115 | # source://parallel//lib/parallel.rb#35 116 | class Parallel::ExceptionWrapper 117 | # @return [ExceptionWrapper] a new instance of ExceptionWrapper 118 | # 119 | # source://parallel//lib/parallel.rb#38 120 | def initialize(exception); end 121 | 122 | # Returns the value of attribute exception. 123 | # 124 | # source://parallel//lib/parallel.rb#36 125 | def exception; end 126 | end 127 | 128 | # source://parallel//lib/parallel.rb#101 129 | class Parallel::JobFactory 130 | # @return [JobFactory] a new instance of JobFactory 131 | # 132 | # source://parallel//lib/parallel.rb#102 133 | def initialize(source, mutex); end 134 | 135 | # source://parallel//lib/parallel.rb#110 136 | def next; end 137 | 138 | # generate item that is sent to workers 139 | # just index is faster + less likely to blow up with unserializable errors 140 | # 141 | # source://parallel//lib/parallel.rb#139 142 | def pack(item, index); end 143 | 144 | # source://parallel//lib/parallel.rb#129 145 | def size; end 146 | 147 | # unpack item that is sent to workers 148 | # 149 | # source://parallel//lib/parallel.rb#144 150 | def unpack(data); end 151 | 152 | private 153 | 154 | # @return [Boolean] 155 | # 156 | # source://parallel//lib/parallel.rb#150 157 | def producer?; end 158 | 159 | # source://parallel//lib/parallel.rb#154 160 | def queue_wrapper(array); end 161 | end 162 | 163 | # source://parallel//lib/parallel.rb#23 164 | class Parallel::Kill < ::Parallel::Break; end 165 | 166 | # TODO: inline this method into parallel.rb and kill physical_processor_count in next major release 167 | # 168 | # source://parallel//lib/parallel/processor_count.rb#4 169 | module Parallel::ProcessorCount 170 | # Number of physical processor cores on the current system. 171 | # 172 | # source://parallel//lib/parallel/processor_count.rb#12 173 | def physical_processor_count; end 174 | 175 | # Number of processors seen by the OS, used for process scheduling 176 | # 177 | # source://parallel//lib/parallel/processor_count.rb#6 178 | def processor_count; end 179 | end 180 | 181 | # source://parallel//lib/parallel.rb#9 182 | Parallel::Stop = T.let(T.unsafe(nil), Object) 183 | 184 | # source://parallel//lib/parallel.rb#26 185 | class Parallel::UndumpableException < ::StandardError 186 | # @return [UndumpableException] a new instance of UndumpableException 187 | # 188 | # source://parallel//lib/parallel.rb#29 189 | def initialize(original); end 190 | 191 | # Returns the value of attribute backtrace. 192 | # 193 | # source://parallel//lib/parallel.rb#27 194 | def backtrace; end 195 | end 196 | 197 | # source://parallel//lib/parallel.rb#159 198 | class Parallel::UserInterruptHandler 199 | class << self 200 | # source://parallel//lib/parallel.rb#184 201 | def kill(thing); end 202 | 203 | # kill all these pids or threads if user presses Ctrl+c 204 | # 205 | # source://parallel//lib/parallel.rb#164 206 | def kill_on_ctrl_c(pids, options); end 207 | 208 | private 209 | 210 | # source://parallel//lib/parallel.rb#208 211 | def restore_interrupt(old, signal); end 212 | 213 | # source://parallel//lib/parallel.rb#193 214 | def trap_interrupt(signal); end 215 | end 216 | end 217 | 218 | # source://parallel//lib/parallel.rb#160 219 | Parallel::UserInterruptHandler::INTERRUPT_SIGNAL = T.let(T.unsafe(nil), Symbol) 220 | 221 | # source://parallel//lib/parallel/version.rb#3 222 | Parallel::VERSION = T.let(T.unsafe(nil), String) 223 | 224 | # source://parallel//lib/parallel/version.rb#3 225 | Parallel::Version = T.let(T.unsafe(nil), String) 226 | 227 | # source://parallel//lib/parallel.rb#54 228 | class Parallel::Worker 229 | # @return [Worker] a new instance of Worker 230 | # 231 | # source://parallel//lib/parallel.rb#58 232 | def initialize(read, write, pid); end 233 | 234 | # might be passed to started_processes and simultaneously closed by another thread 235 | # when running in isolation mode, so we have to check if it is closed before closing 236 | # 237 | # source://parallel//lib/parallel.rb#71 238 | def close_pipes; end 239 | 240 | # Returns the value of attribute pid. 241 | # 242 | # source://parallel//lib/parallel.rb#55 243 | def pid; end 244 | 245 | # Returns the value of attribute read. 246 | # 247 | # source://parallel//lib/parallel.rb#55 248 | def read; end 249 | 250 | # source://parallel//lib/parallel.rb#64 251 | def stop; end 252 | 253 | # Returns the value of attribute thread. 254 | # 255 | # source://parallel//lib/parallel.rb#56 256 | def thread; end 257 | 258 | # Sets the attribute thread 259 | # 260 | # @param value the value to set the attribute thread to. 261 | # 262 | # source://parallel//lib/parallel.rb#56 263 | def thread=(_arg0); end 264 | 265 | # source://parallel//lib/parallel.rb#76 266 | def work(data); end 267 | 268 | # Returns the value of attribute write. 269 | # 270 | # source://parallel//lib/parallel.rb#55 271 | def write; end 272 | 273 | private 274 | 275 | # source://parallel//lib/parallel.rb#94 276 | def wait; end 277 | end 278 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/racc@1.6.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `racc` gem. 5 | # Please instead update this file by running `bin/tapioca gem racc`. 6 | 7 | # source://racc//lib/racc/parser.rb#23 8 | ParseError = Racc::ParseError 9 | 10 | # source://racc//lib/racc/info.rb#16 11 | Racc::Copyright = T.let(T.unsafe(nil), String) 12 | 13 | # source://racc//lib/racc/parser.rb#186 14 | class Racc::Parser 15 | # source://racc//lib/racc/parser.rb#281 16 | def _racc_do_parse_rb(arg, in_debug); end 17 | 18 | # source://racc//lib/racc/parser.rb#481 19 | def _racc_do_reduce(arg, act); end 20 | 21 | # common 22 | # 23 | # source://racc//lib/racc/parser.rb#384 24 | def _racc_evalact(act, arg); end 25 | 26 | # source://racc//lib/racc/parser.rb#234 27 | def _racc_init_sysvars; end 28 | 29 | # source://racc//lib/racc/parser.rb#222 30 | def _racc_setup; end 31 | 32 | # source://racc//lib/racc/parser.rb#331 33 | def _racc_yyparse_rb(recv, mid, arg, c_debug); end 34 | 35 | # The method to fetch next token. 36 | # If you use #do_parse method, you must implement #next_token. 37 | # 38 | # The format of return value is [TOKEN_SYMBOL, VALUE]. 39 | # +token-symbol+ is represented by Ruby's symbol by default, e.g. :IDENT 40 | # for 'IDENT'. ";" (String) for ';'. 41 | # 42 | # The final symbol (End of file) must be false. 43 | # 44 | # @raise [NotImplementedError] 45 | # 46 | # source://racc//lib/racc/parser.rb#277 47 | def next_token; end 48 | 49 | # This method is called when a parse error is found. 50 | # 51 | # ERROR_TOKEN_ID is an internal ID of token which caused error. 52 | # You can get string representation of this ID by calling 53 | # #token_to_str. 54 | # 55 | # ERROR_VALUE is a value of error token. 56 | # 57 | # value_stack is a stack of symbol values. 58 | # DO NOT MODIFY this object. 59 | # 60 | # This method raises ParseError by default. 61 | # 62 | # If this method returns, parsers enter "error recovering mode". 63 | # 64 | # @raise [ParseError] 65 | # 66 | # source://racc//lib/racc/parser.rb#537 67 | def on_error(t, val, vstack); end 68 | 69 | # source://racc//lib/racc/parser.rb#586 70 | def racc_accept; end 71 | 72 | # source://racc//lib/racc/parser.rb#591 73 | def racc_e_pop(state, tstack, vstack); end 74 | 75 | # source://racc//lib/racc/parser.rb#598 76 | def racc_next_state(curstate, state); end 77 | 78 | # source://racc//lib/racc/parser.rb#604 79 | def racc_print_stacks(t, v); end 80 | 81 | # source://racc//lib/racc/parser.rb#613 82 | def racc_print_states(s); end 83 | 84 | # For debugging output 85 | # 86 | # source://racc//lib/racc/parser.rb#560 87 | def racc_read_token(t, tok, val); end 88 | 89 | # source://racc//lib/racc/parser.rb#573 90 | def racc_reduce(toks, sim, tstack, vstack); end 91 | 92 | # source://racc//lib/racc/parser.rb#567 93 | def racc_shift(tok, tstack, vstack); end 94 | 95 | # source://racc//lib/racc/parser.rb#620 96 | def racc_token2str(tok); end 97 | 98 | # Convert internal ID of token symbol to the string. 99 | # 100 | # source://racc//lib/racc/parser.rb#626 101 | def token_to_str(t); end 102 | 103 | # Exit parser. 104 | # Return value is +Symbol_Value_Stack[0]+. 105 | # 106 | # source://racc//lib/racc/parser.rb#550 107 | def yyaccept; end 108 | 109 | # Leave error recovering mode. 110 | # 111 | # source://racc//lib/racc/parser.rb#555 112 | def yyerrok; end 113 | 114 | # Enter error recovering mode. 115 | # This method does not call #on_error. 116 | # 117 | # source://racc//lib/racc/parser.rb#544 118 | def yyerror; end 119 | 120 | class << self 121 | # source://racc//lib/racc/parser.rb#218 122 | def racc_runtime_type; end 123 | end 124 | end 125 | 126 | # source://racc//lib/racc/parser.rb#207 127 | Racc::Parser::Racc_Main_Parsing_Routine = T.let(T.unsafe(nil), Symbol) 128 | 129 | Racc::Parser::Racc_Runtime_Core_Id_C = T.let(T.unsafe(nil), String) 130 | 131 | # source://racc//lib/racc/parser.rb#209 132 | Racc::Parser::Racc_Runtime_Core_Version = T.let(T.unsafe(nil), String) 133 | 134 | Racc::Parser::Racc_Runtime_Core_Version_C = T.let(T.unsafe(nil), String) 135 | 136 | # source://racc//lib/racc/parser.rb#189 137 | Racc::Parser::Racc_Runtime_Core_Version_R = T.let(T.unsafe(nil), String) 138 | 139 | # source://racc//lib/racc/parser.rb#210 140 | Racc::Parser::Racc_Runtime_Type = T.let(T.unsafe(nil), String) 141 | 142 | # source://racc//lib/racc/parser.rb#188 143 | Racc::Parser::Racc_Runtime_Version = T.let(T.unsafe(nil), String) 144 | 145 | # source://racc//lib/racc/parser.rb#208 146 | Racc::Parser::Racc_YY_Parse_Method = T.let(T.unsafe(nil), Symbol) 147 | 148 | # source://racc//lib/racc/parser.rb#183 149 | Racc::Racc_No_Extensions = T.let(T.unsafe(nil), FalseClass) 150 | 151 | # source://racc//lib/racc/info.rb#14 152 | Racc::VERSION = T.let(T.unsafe(nil), String) 153 | 154 | # source://racc//lib/racc/info.rb#15 155 | Racc::Version = T.let(T.unsafe(nil), String) 156 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/rails@7.0.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `rails` gem. 5 | # Please instead update this file by running `bin/tapioca gem rails`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/timeout@0.3.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `timeout` gem. 5 | # Please instead update this file by running `bin/tapioca gem timeout`. 6 | 7 | # source://timeout//lib/timeout.rb#25 8 | module Timeout 9 | private 10 | 11 | # Perform an operation in a block, raising an error if it takes longer than 12 | # +sec+ seconds to complete. 13 | # 14 | # +sec+:: Number of seconds to wait for the block to terminate. Any number 15 | # may be used, including Floats to specify fractional seconds. A 16 | # value of 0 or +nil+ will execute the block without any timeout. 17 | # +klass+:: Exception Class to raise if the block fails to terminate 18 | # in +sec+ seconds. Omitting will use the default, Timeout::Error 19 | # +message+:: Error message to raise with Exception Class. 20 | # Omitting will use the default, "execution expired" 21 | # 22 | # Returns the result of the block *if* the block completed before 23 | # +sec+ seconds, otherwise throws an exception, based on the value of +klass+. 24 | # 25 | # The exception thrown to terminate the given block cannot be rescued inside 26 | # the block unless +klass+ is given explicitly. However, the block can use 27 | # ensure to prevent the handling of the exception. For that reason, this 28 | # method cannot be relied on to enforce timeouts for untrusted blocks. 29 | # 30 | # If a scheduler is defined, it will be used to handle the timeout by invoking 31 | # Scheduler#timeout_after. 32 | # 33 | # Note that this is both a method of module Timeout, so you can include 34 | # Timeout into your classes so they have a #timeout method, as well as 35 | # a module method, so you can call it directly as Timeout.timeout(). 36 | # 37 | # source://timeout//lib/timeout.rb#162 38 | def timeout(sec, klass = T.unsafe(nil), message = T.unsafe(nil), &block); end 39 | 40 | class << self 41 | # source://timeout//lib/timeout.rb#126 42 | def ensure_timeout_thread_created; end 43 | 44 | # Perform an operation in a block, raising an error if it takes longer than 45 | # +sec+ seconds to complete. 46 | # 47 | # +sec+:: Number of seconds to wait for the block to terminate. Any number 48 | # may be used, including Floats to specify fractional seconds. A 49 | # value of 0 or +nil+ will execute the block without any timeout. 50 | # +klass+:: Exception Class to raise if the block fails to terminate 51 | # in +sec+ seconds. Omitting will use the default, Timeout::Error 52 | # +message+:: Error message to raise with Exception Class. 53 | # Omitting will use the default, "execution expired" 54 | # 55 | # Returns the result of the block *if* the block completed before 56 | # +sec+ seconds, otherwise throws an exception, based on the value of +klass+. 57 | # 58 | # The exception thrown to terminate the given block cannot be rescued inside 59 | # the block unless +klass+ is given explicitly. However, the block can use 60 | # ensure to prevent the handling of the exception. For that reason, this 61 | # method cannot be relied on to enforce timeouts for untrusted blocks. 62 | # 63 | # If a scheduler is defined, it will be used to handle the timeout by invoking 64 | # Scheduler#timeout_after. 65 | # 66 | # Note that this is both a method of module Timeout, so you can include 67 | # Timeout into your classes so they have a #timeout method, as well as 68 | # a module method, so you can call it directly as Timeout.timeout(). 69 | # 70 | # source://timeout//lib/timeout.rb#162 71 | def timeout(sec, klass = T.unsafe(nil), message = T.unsafe(nil), &block); end 72 | 73 | private 74 | 75 | # source://timeout//lib/timeout.rb#100 76 | def create_timeout_thread; end 77 | end 78 | end 79 | 80 | # :stopdoc: 81 | # 82 | # source://timeout//lib/timeout.rb#53 83 | Timeout::CONDVAR = T.let(T.unsafe(nil), Thread::ConditionVariable) 84 | 85 | # Raised by Timeout.timeout when the block times out. 86 | # 87 | # source://timeout//lib/timeout.rb#29 88 | class Timeout::Error < ::RuntimeError 89 | # source://timeout//lib/timeout.rb#39 90 | def exception(*_arg0); end 91 | 92 | # Returns the value of attribute thread. 93 | # 94 | # source://timeout//lib/timeout.rb#30 95 | def thread; end 96 | 97 | class << self 98 | # source://timeout//lib/timeout.rb#32 99 | def catch(*args); end 100 | end 101 | end 102 | 103 | # source://timeout//lib/timeout.rb#54 104 | Timeout::QUEUE = T.let(T.unsafe(nil), Thread::Queue) 105 | 106 | # source://timeout//lib/timeout.rb#55 107 | Timeout::QUEUE_MUTEX = T.let(T.unsafe(nil), Thread::Mutex) 108 | 109 | # source://timeout//lib/timeout.rb#60 110 | class Timeout::Request 111 | # @return [Request] a new instance of Request 112 | # 113 | # source://timeout//lib/timeout.rb#63 114 | def initialize(thread, timeout, exception_class, message); end 115 | 116 | # Returns the value of attribute deadline. 117 | # 118 | # source://timeout//lib/timeout.rb#61 119 | def deadline; end 120 | 121 | # @return [Boolean] 122 | # 123 | # source://timeout//lib/timeout.rb#73 124 | def done?; end 125 | 126 | # @return [Boolean] 127 | # 128 | # source://timeout//lib/timeout.rb#79 129 | def expired?(now); end 130 | 131 | # source://timeout//lib/timeout.rb#92 132 | def finished; end 133 | 134 | # source://timeout//lib/timeout.rb#83 135 | def interrupt; end 136 | end 137 | 138 | # source://timeout//lib/timeout.rb#56 139 | Timeout::TIMEOUT_THREAD_MUTEX = T.let(T.unsafe(nil), Thread::Mutex) 140 | 141 | # source://timeout//lib/timeout.rb#26 142 | Timeout::VERSION = T.let(T.unsafe(nil), String) 143 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/unparser@0.6.5.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `unparser` gem. 5 | # Please instead update this file by running `bin/tapioca gem unparser`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/websocket-driver@0.7.5.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `websocket-driver` gem. 5 | # Please instead update this file by running `bin/tapioca gem websocket-driver`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/websocket-extensions@0.1.5.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `websocket-extensions` gem. 5 | # Please instead update this file by running `bin/tapioca gem websocket-extensions`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/yard-sorbet@0.7.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `yard-sorbet` gem. 5 | # Please instead update this file by running `bin/tapioca gem yard-sorbet`. 6 | 7 | class YARD::Handlers::Ruby::ClassHandler < ::YARD::Handlers::Ruby::Base 8 | include ::YARDSorbet::Handlers::StructClassHandler 9 | end 10 | 11 | # source://yard-sorbet//lib/yard-sorbet/version.rb#5 12 | module YARDSorbet; end 13 | 14 | # Extract & re-add directives to a docstring 15 | # 16 | # source://yard-sorbet//lib/yard-sorbet/directives.rb#6 17 | module YARDSorbet::Directives 18 | class << self 19 | # source://yard-sorbet//lib/yard-sorbet/directives.rb#21 20 | sig { params(docstring: ::String, directives: T::Array[::String]).void } 21 | def add_directives(docstring, directives); end 22 | 23 | # source://yard-sorbet//lib/yard-sorbet/directives.rb#10 24 | sig { params(docstring: T.nilable(::String)).returns([::YARD::Docstring, T::Array[::String]]) } 25 | def extract_directives(docstring); end 26 | end 27 | end 28 | 29 | # Custom YARD Handlers 30 | # 31 | # @see https://rubydoc.info/gems/yard/YARD/Handlers/Base YARD Base Handler documentation 32 | # 33 | # source://yard-sorbet//lib/yard-sorbet/handlers.rb#7 34 | module YARDSorbet::Handlers; end 35 | 36 | # Apllies an `@abstract` tag to `abstract!`/`interface!` modules (if not alerady present). 37 | # 38 | # source://yard-sorbet//lib/yard-sorbet/handlers/abstract_dsl_handler.rb#7 39 | class YARDSorbet::Handlers::AbstractDSLHandler < ::YARD::Handlers::Ruby::Base 40 | # source://yard-sorbet//lib/yard-sorbet/handlers/abstract_dsl_handler.rb#21 41 | sig { void } 42 | def process; end 43 | end 44 | 45 | # Extra text for class namespaces 46 | # 47 | # source://yard-sorbet//lib/yard-sorbet/handlers/abstract_dsl_handler.rb#18 48 | YARDSorbet::Handlers::AbstractDSLHandler::CLASS_TAG_TEXT = T.let(T.unsafe(nil), String) 49 | 50 | # The text accompanying the `@abstract` tag. 51 | # 52 | # @see https://github.com/lsegal/yard/blob/main/templates/default/docstring/html/abstract.erb The `@abstract` tag template 53 | # 54 | # source://yard-sorbet//lib/yard-sorbet/handlers/abstract_dsl_handler.rb#16 55 | YARDSorbet::Handlers::AbstractDSLHandler::TAG_TEXT = T.let(T.unsafe(nil), String) 56 | 57 | # Handle `enums` calls, registering enum values as constants 58 | # 59 | # source://yard-sorbet//lib/yard-sorbet/handlers/enums_handler.rb#7 60 | class YARDSorbet::Handlers::EnumsHandler < ::YARD::Handlers::Ruby::Base 61 | # source://yard-sorbet//lib/yard-sorbet/handlers/enums_handler.rb#14 62 | sig { void } 63 | def process; end 64 | 65 | private 66 | 67 | # source://yard-sorbet//lib/yard-sorbet/handlers/enums_handler.rb#29 68 | sig { params(node: ::YARD::Parser::Ruby::AstNode).returns(T::Boolean) } 69 | def const_assign_node?(node); end 70 | end 71 | 72 | # Extends any modules included via `mixes_in_class_methods` 73 | # 74 | # @see https://sorbet.org/docs/abstract#interfaces-and-the-included-hook Sorbet `mixes_in_class_methods` documentation 75 | # 76 | # source://yard-sorbet//lib/yard-sorbet/handlers/include_handler.rb#9 77 | class YARDSorbet::Handlers::IncludeHandler < ::YARD::Handlers::Ruby::Base 78 | # source://yard-sorbet//lib/yard-sorbet/handlers/include_handler.rb#16 79 | sig { void } 80 | def process; end 81 | 82 | private 83 | 84 | # source://yard-sorbet//lib/yard-sorbet/handlers/include_handler.rb#30 85 | sig { returns(::YARD::CodeObjects::NamespaceObject) } 86 | def included_in; end 87 | end 88 | 89 | # Tracks modules that invoke `mixes_in_class_methods` for use in {IncludeHandler} 90 | # 91 | # @see https://sorbet.org/docs/abstract#interfaces-and-the-included-hook Sorbet `mixes_in_class_methods` documentation 92 | # 93 | # source://yard-sorbet//lib/yard-sorbet/handlers/mixes_in_class_methods_handler.rb#9 94 | class YARDSorbet::Handlers::MixesInClassMethodsHandler < ::YARD::Handlers::Ruby::Base 95 | # source://yard-sorbet//lib/yard-sorbet/handlers/mixes_in_class_methods_handler.rb#23 96 | sig { void } 97 | def process; end 98 | 99 | class << self 100 | # source://yard-sorbet//lib/yard-sorbet/handlers/mixes_in_class_methods_handler.rb#18 101 | sig { params(code_obj: ::String).returns(T.nilable(::String)) } 102 | def mixed_in_class_methods(code_obj); end 103 | end 104 | end 105 | 106 | # A YARD Handler for Sorbet type declarations 107 | # 108 | # source://yard-sorbet//lib/yard-sorbet/handlers/sig_handler.rb#7 109 | class YARDSorbet::Handlers::SigHandler < ::YARD::Handlers::Ruby::Base 110 | # Swap the method definition docstring and the sig docstring. 111 | # Parse relevant parts of the `sig` and include them as well. 112 | # 113 | # source://yard-sorbet//lib/yard-sorbet/handlers/sig_handler.rb#20 114 | sig { void } 115 | def process; end 116 | 117 | private 118 | 119 | # source://yard-sorbet//lib/yard-sorbet/handlers/sig_handler.rb#52 120 | sig do 121 | params( 122 | method_node: ::YARD::Parser::Ruby::AstNode, 123 | node: ::YARD::Parser::Ruby::AstNode, 124 | docstring: ::YARD::Docstring 125 | ).void 126 | end 127 | def parse_params(method_node, node, docstring); end 128 | 129 | # source://yard-sorbet//lib/yard-sorbet/handlers/sig_handler.rb#64 130 | sig { params(node: ::YARD::Parser::Ruby::AstNode, docstring: ::YARD::Docstring).void } 131 | def parse_return(node, docstring); end 132 | 133 | # source://yard-sorbet//lib/yard-sorbet/handlers/sig_handler.rb#32 134 | sig { params(method_node: ::YARD::Parser::Ruby::AstNode, docstring: ::YARD::Docstring).void } 135 | def parse_sig(method_node, docstring); end 136 | end 137 | 138 | # These node types attached to sigs represent attr_* declarations 139 | # 140 | # source://yard-sorbet//lib/yard-sorbet/handlers/sig_handler.rb#14 141 | YARDSorbet::Handlers::SigHandler::ATTR_NODE_TYPES = T.let(T.unsafe(nil), Array) 142 | 143 | # Class-level handler that folds all `const` and `prop` declarations into the constructor documentation 144 | # this needs to be injected as a module otherwise the default Class handler will overwrite documentation 145 | # 146 | # @note this module is included in `YARD::Handlers::Ruby::ClassHandler` 147 | # 148 | # source://yard-sorbet//lib/yard-sorbet/handlers/struct_class_handler.rb#10 149 | module YARDSorbet::Handlers::StructClassHandler 150 | # source://yard-sorbet//lib/yard-sorbet/handlers/struct_class_handler.rb#14 151 | sig { void } 152 | def process; end 153 | 154 | private 155 | 156 | # source://yard-sorbet//lib/yard-sorbet/handlers/struct_class_handler.rb#50 157 | sig do 158 | params( 159 | object: ::YARD::CodeObjects::MethodObject, 160 | props: T::Array[::YARDSorbet::TStructProp], 161 | docstring: ::YARD::Docstring, 162 | directives: T::Array[::String] 163 | ).void 164 | end 165 | def decorate_t_struct_init(object, props, docstring, directives); end 166 | 167 | # Create a virtual `initialize` method with all the `prop`/`const` arguments 168 | # 169 | # source://yard-sorbet//lib/yard-sorbet/handlers/struct_class_handler.rb#30 170 | sig { params(props: T::Array[::YARDSorbet::TStructProp], class_ns: ::YARD::CodeObjects::ClassObject).void } 171 | def process_t_struct_props(props, class_ns); end 172 | 173 | # source://yard-sorbet//lib/yard-sorbet/handlers/struct_class_handler.rb#60 174 | sig { params(props: T::Array[::YARDSorbet::TStructProp]).returns(T::Array[[::String, T.nilable(::String)]]) } 175 | def to_object_parameters(props); end 176 | end 177 | 178 | # Handles all `const`/`prop` calls, creating accessor methods, and compiles them for later usage at the class level 179 | # in creating a constructor 180 | # 181 | # source://yard-sorbet//lib/yard-sorbet/handlers/struct_prop_handler.rb#8 182 | class YARDSorbet::Handlers::StructPropHandler < ::YARD::Handlers::Ruby::Base 183 | # source://yard-sorbet//lib/yard-sorbet/handlers/struct_prop_handler.rb#15 184 | sig { void } 185 | def process; end 186 | 187 | private 188 | 189 | # Add the source and docstring to the method object 190 | # 191 | # source://yard-sorbet//lib/yard-sorbet/handlers/struct_prop_handler.rb#28 192 | sig { params(object: ::YARD::CodeObjects::MethodObject, prop: ::YARDSorbet::TStructProp).void } 193 | def decorate_object(object, prop); end 194 | 195 | # Get the default prop value 196 | # 197 | # source://yard-sorbet//lib/yard-sorbet/handlers/struct_prop_handler.rb#39 198 | sig { returns(T.nilable(::String)) } 199 | def default_value; end 200 | 201 | # source://yard-sorbet//lib/yard-sorbet/handlers/struct_prop_handler.rb#44 202 | sig { params(name: ::String).returns(::YARDSorbet::TStructProp) } 203 | def make_prop(name); end 204 | 205 | # Register the field explicitly as an attribute. 206 | # While `const` attributes are immutable, `prop` attributes may be reassigned. 207 | # 208 | # source://yard-sorbet//lib/yard-sorbet/handlers/struct_prop_handler.rb#57 209 | sig { params(object: ::YARD::CodeObjects::MethodObject, name: ::String).void } 210 | def register_attrs(object, name); end 211 | 212 | # Store the prop for use in the constructor definition 213 | # 214 | # source://yard-sorbet//lib/yard-sorbet/handlers/struct_prop_handler.rb#65 215 | sig { params(prop: ::YARDSorbet::TStructProp).void } 216 | def update_state(prop); end 217 | end 218 | 219 | # Helper methods for working with `YARD` AST Nodes 220 | # 221 | # source://yard-sorbet//lib/yard-sorbet/node_utils.rb#6 222 | module YARDSorbet::NodeUtils 223 | class << self 224 | # Traverse AST nodes in breadth-first order 225 | # 226 | # @note This will skip over some node types. 227 | # @yield [YARD::Parser::Ruby::AstNode] 228 | # 229 | # source://yard-sorbet//lib/yard-sorbet/node_utils.rb#22 230 | sig do 231 | params( 232 | node: ::YARD::Parser::Ruby::AstNode, 233 | _blk: T.proc.params(n: ::YARD::Parser::Ruby::AstNode).void 234 | ).void 235 | end 236 | def bfs_traverse(node, &_blk); end 237 | 238 | # Gets the node that a sorbet `sig` can be attached do, bypassing visisbility modifiers and the like 239 | # 240 | # source://yard-sorbet//lib/yard-sorbet/node_utils.rb#34 241 | sig do 242 | params( 243 | node: ::YARD::Parser::Ruby::AstNode 244 | ).returns(T.any(::YARD::Parser::Ruby::MethodCallNode, ::YARD::Parser::Ruby::MethodDefinitionNode)) 245 | end 246 | def get_method_node(node); end 247 | 248 | # Find and return the adjacent node (ascending) 249 | # 250 | # @raise [IndexError] if the node does not have an adjacent sibling (ascending) 251 | # 252 | # source://yard-sorbet//lib/yard-sorbet/node_utils.rb#48 253 | sig { params(node: ::YARD::Parser::Ruby::AstNode).returns(::YARD::Parser::Ruby::AstNode) } 254 | def sibling_node(node); end 255 | end 256 | end 257 | 258 | # Command node types that can have type signatures 259 | # 260 | # source://yard-sorbet//lib/yard-sorbet/node_utils.rb#10 261 | YARDSorbet::NodeUtils::ATTRIBUTE_METHODS = T.let(T.unsafe(nil), Array) 262 | 263 | # Skip these method contents during BFS node traversal, they can have their own nested types via `T.Proc` 264 | # 265 | # source://yard-sorbet//lib/yard-sorbet/node_utils.rb#12 266 | YARDSorbet::NodeUtils::SKIP_METHOD_CONTENTS = T.let(T.unsafe(nil), Array) 267 | 268 | # Node types that can have type signatures 269 | # 270 | # source://yard-sorbet//lib/yard-sorbet/node_utils.rb#14 271 | YARDSorbet::NodeUtils::SigableNode = T.type_alias { T.any(::YARD::Parser::Ruby::MethodCallNode, ::YARD::Parser::Ruby::MethodDefinitionNode) } 272 | 273 | # Translate `sig` type syntax to `YARD` type syntax. 274 | # 275 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#6 276 | module YARDSorbet::SigToYARD 277 | class << self 278 | # @see https://yardoc.org/types.html 279 | # 280 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#21 281 | sig { params(node: ::YARD::Parser::Ruby::AstNode).returns(T::Array[::String]) } 282 | def convert(node); end 283 | 284 | private 285 | 286 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#54 287 | sig { params(node: ::YARD::Parser::Ruby::AstNode).returns(::String) } 288 | def build_generic_type(node); end 289 | 290 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#63 291 | sig { params(node: ::YARD::Parser::Ruby::AstNode).returns(T::Array[::String]) } 292 | def convert_aref(node); end 293 | 294 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#75 295 | sig { params(node: ::YARD::Parser::Ruby::AstNode).returns([::String]) } 296 | def convert_array(node); end 297 | 298 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#83 299 | sig { params(node: ::YARD::Parser::Ruby::MethodCallNode).returns(T::Array[::String]) } 300 | def convert_call(node); end 301 | 302 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#88 303 | sig { params(node: ::YARD::Parser::Ruby::AstNode).returns([::String]) } 304 | def convert_collection(node); end 305 | 306 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#95 307 | sig { params(node: ::YARD::Parser::Ruby::AstNode).returns([::String]) } 308 | def convert_hash(node); end 309 | 310 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#103 311 | sig { params(node: ::YARD::Parser::Ruby::AstNode).returns(T::Array[::String]) } 312 | def convert_list(node); end 313 | 314 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#27 315 | sig { params(node: ::YARD::Parser::Ruby::AstNode).returns(T::Array[::String]) } 316 | def convert_node(node); end 317 | 318 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#36 319 | sig { params(node: ::YARD::Parser::Ruby::AstNode).returns(T::Array[::String]) } 320 | def convert_node_type(node); end 321 | 322 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#108 323 | sig { params(node_source: ::String).returns([::String]) } 324 | def convert_ref(node_source); end 325 | 326 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#113 327 | sig { params(node: ::YARD::Parser::Ruby::MethodCallNode).returns(T::Array[::String]) } 328 | def convert_t_method(node); end 329 | 330 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#125 331 | sig { params(node: ::YARD::Parser::Ruby::AstNode).returns([::String]) } 332 | def convert_unknown(node); end 333 | end 334 | end 335 | 336 | # source://yard-sorbet//lib/yard-sorbet/sig_to_yard.rb#9 337 | YARDSorbet::SigToYARD::REF_TYPES = T.let(T.unsafe(nil), Hash) 338 | 339 | # Used to store the details of a `T::Struct` `prop` definition 340 | # 341 | # source://yard-sorbet//lib/yard-sorbet/t_struct_prop.rb#6 342 | class YARDSorbet::TStructProp < ::T::Struct 343 | const :default, T.nilable(::String) 344 | const :doc, ::String 345 | const :prop_name, ::String 346 | const :source, ::String 347 | const :types, T::Array[::String] 348 | 349 | class << self 350 | # source://sorbet-runtime/0.5.10520/lib/types/struct.rb#13 351 | def inherited(s); end 352 | end 353 | end 354 | 355 | # Helper methods for working with `YARD` tags 356 | # 357 | # source://yard-sorbet//lib/yard-sorbet/tag_utils.rb#6 358 | module YARDSorbet::TagUtils 359 | class << self 360 | # source://yard-sorbet//lib/yard-sorbet/tag_utils.rb#13 361 | sig do 362 | params( 363 | docstring: ::YARD::Docstring, 364 | tag_name: ::String, 365 | name: T.nilable(::String) 366 | ).returns(T.nilable(::YARD::Tags::Tag)) 367 | end 368 | def find_tag(docstring, tag_name, name); end 369 | 370 | # Create or update a `YARD` tag with type information 371 | # 372 | # source://yard-sorbet//lib/yard-sorbet/tag_utils.rb#27 373 | sig do 374 | params( 375 | docstring: ::YARD::Docstring, 376 | tag_name: ::String, 377 | types: T.nilable(T::Array[::String]), 378 | name: T.nilable(::String), 379 | text: ::String 380 | ).void 381 | end 382 | def upsert_tag(docstring, tag_name, types = T.unsafe(nil), name = T.unsafe(nil), text = T.unsafe(nil)); end 383 | end 384 | end 385 | 386 | # {https://rubygems.org/gems/yard-sorbet Version history} 387 | # 388 | # source://yard-sorbet//lib/yard-sorbet/version.rb#7 389 | YARDSorbet::VERSION = T.let(T.unsafe(nil), String) 390 | -------------------------------------------------------------------------------- /sorbet/tapioca/config.yml: -------------------------------------------------------------------------------- 1 | gem: 2 | # Add your `gem` command parameters here: 3 | # 4 | # exclude: 5 | # - gem_name 6 | # doc: true 7 | # workers: 5 8 | dsl: 9 | # Add your `dsl` command parameters here: 10 | # 11 | # exclude: 12 | # - SomeGeneratorName 13 | # workers: 5 14 | -------------------------------------------------------------------------------- /sorbet/tapioca/require.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'minitest/autorun' 5 | require 'mocha/minitest' 6 | require 'packwerk' 7 | require 'packwerk/privacy/checker' 8 | require 'packwerk/privacy/package' 9 | require 'packwerk/privacy/validator' 10 | require 'rails' 11 | require 'rails/all' 12 | require 'sorbet-runtime' 13 | require 'zeitwerk' 14 | -------------------------------------------------------------------------------- /test/fixtures/extended/package.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/test/fixtures/extended/package.yml -------------------------------------------------------------------------------- /test/fixtures/extended/packwerk.yml: -------------------------------------------------------------------------------- 1 | parallel: false 2 | require: 3 | - packwerk-extensions 4 | -------------------------------------------------------------------------------- /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/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/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/minimal/components/sales/package.yml: -------------------------------------------------------------------------------- 1 | --- 2 | metadata: 3 | stewards: 4 | - "@Shopify/sales" 5 | slack_channels: 6 | - "#sales" 7 | -------------------------------------------------------------------------------- /test/fixtures/minimal/config/environment.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/test/fixtures/minimal/config/environment.rb -------------------------------------------------------------------------------- /test/fixtures/minimal/package.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/test/fixtures/minimal/package.yml -------------------------------------------------------------------------------- /test/fixtures/minimal/packwerk.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - "**/*.rb" 3 | -------------------------------------------------------------------------------- /test/fixtures/minimal/utility/package.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/test/fixtures/minimal/utility/package.yml -------------------------------------------------------------------------------- /test/fixtures/package_todo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | buyers: 3 | "::Buyers::Document": 4 | violations: 5 | - privacy 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/package_todo_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 | -------------------------------------------------------------------------------- /test/fixtures/skeleton/components/platform/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/test/fixtures/skeleton/components/platform/app/models/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/skeleton/components/sales/app/internal/special.rb: -------------------------------------------------------------------------------- 1 | # typed: ignore 2 | # frozen_string_literal: true 3 | 4 | module Company 5 | class Special 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /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/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/skeleton/components/sales/app/models/order/foo.rb: -------------------------------------------------------------------------------- 1 | # pack_public: true 2 | # typed: ignore 3 | # frozen_string_literal: true 4 | 5 | class Order 6 | class Foo 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/views/order.html.erb: -------------------------------------------------------------------------------- 1 |

Order

2 | -------------------------------------------------------------------------------- /test/fixtures/skeleton/components/sales/package.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - 'components/timeline' 4 | 5 | metadata: 6 | stewards: 7 | - "@Shopify/sales" 8 | slack_channels: 9 | - "#sales" 10 | -------------------------------------------------------------------------------- /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/timeline/app/models/concerns/has_timeline.rb: -------------------------------------------------------------------------------- 1 | # typed: ignore 2 | # frozen_string_literal: true 3 | 4 | module HasTimeline 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/skeleton/components/timeline/app/models/graphql.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | end 3 | -------------------------------------------------------------------------------- /test/fixtures/skeleton/components/timeline/app/models/graphql/private_thing.rb: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | class PrivateThing 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/fixtures/skeleton/components/timeline/app/models/imaginary/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/test/fixtures/skeleton/components/timeline/app/models/imaginary/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/skeleton/components/timeline/app/models/private_thing.rb: -------------------------------------------------------------------------------- 1 | class PrivateThing 2 | end 3 | -------------------------------------------------------------------------------- /test/fixtures/skeleton/components/timeline/app/models/sales/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/test/fixtures/skeleton/components/timeline/app/models/sales/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/skeleton/components/timeline/nested/package.yml: -------------------------------------------------------------------------------- 1 | enforce_dependencies: true 2 | -------------------------------------------------------------------------------- /test/fixtures/skeleton/components/timeline/package.yml: -------------------------------------------------------------------------------- 1 | enforce_privacy: true 2 | private_constants: 3 | - "::PrivateThing" 4 | - "::GraphQL::PrivateThing" 5 | -------------------------------------------------------------------------------- /test/fixtures/skeleton/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/test/fixtures/skeleton/config/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/skeleton/config/environment.rb: -------------------------------------------------------------------------------- 1 | # this file intentionally left blank 2 | -------------------------------------------------------------------------------- /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/fixtures/skeleton/package.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/test/fixtures/skeleton/package.yml -------------------------------------------------------------------------------- /test/fixtures/skeleton/packwerk.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - "**/*.rb" 3 | - "**/*.{blop,rb}" # To test for duplicated files 4 | exclude: 5 | - "**/temp.rb" 6 | parallel: false 7 | -------------------------------------------------------------------------------- /test/fixtures/skeleton/vendor/cache/gems/example/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/test/fixtures/skeleton/vendor/cache/gems/example/models/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/skeleton/vendor/cache/gems/example/package.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/test/fixtures/skeleton/vendor/cache/gems/example/package.yml -------------------------------------------------------------------------------- /test/fixtures/with_unrecognized_config/components/sales/app/models/order.rb: -------------------------------------------------------------------------------- 1 | # typed: ignore 2 | # frozen_string_literal: true 3 | 4 | class Order 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/with_unrecognized_config/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/with_unrecognized_config/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/with_unrecognized_config/components/sales/package.yml: -------------------------------------------------------------------------------- 1 | --- 2 | metadata: 3 | stewards: 4 | - "@Shopify/sales" 5 | slack_channels: 6 | - "#sales" 7 | -------------------------------------------------------------------------------- /test/fixtures/with_unrecognized_config/package.yml: -------------------------------------------------------------------------------- 1 | enforce_folder_privacy: true 2 | 3 | enforcement_globs_ignore: 4 | - enforcements: 5 | - privacy 6 | ignores: 7 | - "**/*" 8 | - "!packs/product_services/serv1/**/*" -------------------------------------------------------------------------------- /test/fixtures/with_unrecognized_config/packwerk.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - "**/*.rb" 3 | -------------------------------------------------------------------------------- /test/fixtures/with_unrecognized_config/utility/package.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyatscale/packwerk-extensions/a103ed51542335e4e08ba30c64f42e191bd7ba4a/test/fixtures/with_unrecognized_config/utility/package.yml -------------------------------------------------------------------------------- /test/integration/extension_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | module Packwerk 7 | module Privacy 8 | class ExtensionTest < Minitest::Test 9 | extend T::Sig 10 | 11 | include ApplicationFixtureHelper 12 | 13 | setup do 14 | setup_application_fixture 15 | end 16 | 17 | teardown do 18 | teardown_application_fixture 19 | end 20 | 21 | test 'extension is properly loaded' do 22 | use_template(:extended) 23 | Packwerk::Checker.all 24 | assert_equal(Packwerk::Checker.all.count, 5) 25 | found_checker = Packwerk::Checker.all.any? do |checker| 26 | T.unsafe(checker).is_a?(Packwerk::Privacy::Checker) 27 | end 28 | assert found_checker 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/integration/extension_with_unrecognized_config_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | module Packwerk 7 | module Privacy 8 | class ExtensionWithUnrecognizedConfigTest < Minitest::Test 9 | extend T::Sig 10 | 11 | include ApplicationFixtureHelper 12 | 13 | setup do 14 | setup_application_fixture 15 | end 16 | 17 | teardown do 18 | teardown_application_fixture 19 | end 20 | 21 | test 'extension is properly loaded' do 22 | use_template(:with_unrecognized_config) 23 | Packwerk::Checker.all 24 | assert_equal(Packwerk::Checker.all.count, 5) 25 | found_checker = Packwerk::Checker.all.any? do |checker| 26 | T.unsafe(checker).is_a?(Packwerk::Privacy::Checker) 27 | end 28 | assert found_checker 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/rails_test_helper.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'rails' 5 | 6 | ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | inflect.acronym 'GraphQL' 8 | end 9 | 10 | class Dummy < Rails::Application 11 | class << self 12 | def skeleton(*path) 13 | ROOT.join('test', 'fixtures', 'skeleton', *path).to_s 14 | end 15 | end 16 | 17 | config.autoloader = :zeitwerk 18 | config.eager_load = false 19 | config.eager_load_paths = [skeleton('components', 'platform', 'app', 'models')] 20 | config.autoload_paths = [skeleton('components', 'sales', 'app', 'models')] 21 | config.autoload_once_paths = [ 22 | skeleton('components', 'timeline', 'app', 'models'), 23 | skeleton('components', 'timeline', 'app', 'models', 'concerns'), 24 | skeleton('vendor', 'cache', 'gems', 'example', 'models') 25 | ] 26 | config.root = skeleton('.') 27 | config.logger = Logger.new('/dev/null') 28 | end 29 | 30 | Rails.application.initialize! 31 | -------------------------------------------------------------------------------- /test/support/application_fixture_helper.rb: -------------------------------------------------------------------------------- 1 | # typed: true 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 | extend T::Helpers 9 | 10 | requires_ancestor { Kernel } 11 | 12 | def setup_application_fixture 13 | @old_working_dir = Dir.pwd 14 | end 15 | 16 | def teardown_application_fixture 17 | Dir.chdir(@old_working_dir) 18 | FileUtils.remove_entry(@app_dir, true) if using_template? 19 | end 20 | 21 | def use_template(template) 22 | raise 'use_template may only be called once per test' if using_template? 23 | 24 | copy_dir("test/fixtures/#{template}") 25 | Dir.chdir(app_dir) 26 | end 27 | 28 | def app_dir 29 | unless using_template? 30 | raise 'You need to set up an application fixture by calling `use_template(:the_template)`.' 31 | end 32 | 33 | @app_dir 34 | end 35 | 36 | def config 37 | @config ||= Packwerk::Configuration.from_path(app_dir) 38 | end 39 | 40 | def package_set 41 | @package_set ||= Packwerk::PackageSet.load_all_from(config.root_path) 42 | end 43 | 44 | def to_app_path(relative_path) 45 | File.join(app_dir, relative_path) 46 | end 47 | 48 | def to_app_paths(*relative_paths) 49 | relative_paths.map { |path| to_app_path(path) } 50 | end 51 | 52 | def merge_into_app_yaml_file(relative_path, hash) 53 | path = to_app_path(relative_path) 54 | YamlFile.new(path).merge(hash) 55 | end 56 | 57 | def remove_app_entry(relative_path) 58 | FileUtils.remove_entry(to_app_path(relative_path)) 59 | end 60 | 61 | def open_app_file(path, mode: 'w+', &block) 62 | expanded_path = to_app_path(File.join(path)) 63 | File.open(expanded_path, mode, &block) 64 | end 65 | 66 | # This gets cleaned up by `teardown_application_fixture` 67 | def write_app_file(path, content) 68 | expanded_path = to_app_path(File.join(*path)) 69 | FileUtils.mkdir_p(Pathname.new(expanded_path).dirname) 70 | File.open(expanded_path, 'w+') do |file| 71 | file.write(content) 72 | file.flush 73 | end 74 | end 75 | 76 | private 77 | 78 | def using_template? 79 | defined? @app_dir 80 | end 81 | 82 | def copy_dir(path) 83 | root = FileUtils.mkdir_p(fixture_path).last 84 | FileUtils.cp_r("#{path}/.", T.must(root)) 85 | @app_dir = root 86 | end 87 | 88 | def fixture_path 89 | T.bind(self, Minitest::Runnable) 90 | File.join(TEMP_FIXTURE_DIR, "#{name}-#{Time.now.strftime('%Y_%m_%d_%H_%M_%S')}") 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/support/factory_helper.rb: -------------------------------------------------------------------------------- 1 | # typed: true 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 | constant_location: 'some/location.rb', 11 | source_location: Packwerk::Node::Location.new(2, 12) 12 | ) 13 | constant = Packwerk::ConstantContext.new( 14 | constant_name, 15 | constant_location, 16 | destination_package 17 | ) 18 | 19 | Packwerk::Privacy::Checker.publicized_locations['some/location.rb'] = false 20 | 21 | Packwerk::Reference.new( 22 | package: source_package, 23 | relative_path: path, 24 | constant: constant, 25 | source_location: source_location 26 | ) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/support/rails_application_fixture_helper.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'rails_test_helper' 5 | require 'zeitwerk' 6 | 7 | module RailsApplicationFixtureHelper 8 | include ApplicationFixtureHelper 9 | 10 | class Autoloaders 11 | include Enumerable 12 | 13 | def initialize 14 | @main = Zeitwerk::Loader.new 15 | @once = Zeitwerk::Loader.new 16 | end 17 | 18 | attr_reader :main, :once 19 | 20 | def each(&block) 21 | yield(main) 22 | yield(once) 23 | end 24 | 25 | def zeitwerk_enabled? 26 | true 27 | end 28 | end 29 | 30 | def use_template(template) 31 | super(template) 32 | 33 | Rails.stubs(:autoloaders).returns(Autoloaders.new) 34 | 35 | case template 36 | when :minimal 37 | set_load_paths_for_minimal_template 38 | when :skeleton 39 | set_load_paths_for_skeleton_template 40 | else 41 | raise "Unknown fixture template #{template}" 42 | end 43 | 44 | root = Pathname.new(app_dir) 45 | Rails.application.config.stubs(:root).returns(root) 46 | end 47 | 48 | private 49 | 50 | def set_load_paths_for_minimal_template 51 | Rails.autoloaders.main.push_dir(*to_app_paths('/components/sales/app/models')) 52 | end 53 | 54 | def set_load_paths_for_skeleton_template 55 | Rails.autoloaders.main.push_dir(*to_app_paths('/components/sales/app/models')) 56 | Rails.autoloaders.main.push_dir(*to_app_paths('components/platform/app/models')) 57 | 58 | Rails.autoloaders.once.push_dir(*to_app_paths('components/timeline/app/models')) 59 | Rails.autoloaders.once.push_dir(*to_app_paths('components/timeline/app/models/concerns')) 60 | Rails.autoloaders.once.push_dir(*to_app_paths('vendor/cache/gems/example/models')) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/support/yaml_file.rb: -------------------------------------------------------------------------------- 1 | # typed: true 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 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | ENV['RAILS_ENV'] = 'test' 5 | 6 | $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) 7 | ROOT = Pathname.new(__dir__).join('..').expand_path 8 | 9 | require 'packwerk-extensions' 10 | require 'packwerk' 11 | require 'minitest/autorun' 12 | require 'mocha/minitest' 13 | require 'support/application_fixture_helper' 14 | require 'support/rails_application_fixture_helper' 15 | require 'support/yaml_file' 16 | require 'support/factory_helper' 17 | require 'support/test_macro' 18 | require 'pry' 19 | 20 | Minitest::Test.extend(TestMacro) 21 | 22 | Mocha.configure do |c| 23 | c.stubbing_non_existent_method = :prevent 24 | end 25 | -------------------------------------------------------------------------------- /test/unit/folder_privacy/checker_test.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | module Packwerk 7 | module FolderPrivacy 8 | class CheckerTest < Minitest::Test 9 | extend T::Sig 10 | include FactoryHelper 11 | include RailsApplicationFixtureHelper 12 | 13 | setup do 14 | setup_application_fixture 15 | end 16 | 17 | teardown do 18 | teardown_application_fixture 19 | end 20 | 21 | true_ = true 22 | 23 | [ 24 | # enforce, pack, referencing pack invalid? note 25 | [false, 'packs/a', 'packs/b/c/d/e/f', false, 'turned off'], 26 | [true_, 'packs/a', 'packs/b', false, 'siblings are ok'], 27 | [true_, 'packs/a', 'packs/c', false, 'siblings are ok'], 28 | [true_, 'packs/a/packs/1', 'packs/a/packs/2', false, 'siblings are ok'], 29 | [true_, 'packs/a/packs/1', 'packs/a/packs', false, 'access from parent is ok'], 30 | [true_, 'packs/a/packs/1', 'packs/a', false, 'access from parent of parent is ok'], 31 | [true_, 'packs/a/packs/1', 'packs', false, 'access from parent of parent is ok'], 32 | [true_, 'packs/a/packs/1', '.', false, 'access from root pack is ok'], 33 | 34 | [true_, 'packs/a', 'packs/b/c', true_, 'not siblings or child'], 35 | [true_, 'packs/a', 'packs/b/packs/c', true_, 'not siblings or child'], 36 | [true_, 'packs/a/packs/1', 'packs/b/packs/1', true_, 'not siblings or child'], 37 | [true_, 'packs/a', 'packs/a/packs/1', true_, 'access to parent not ok'], 38 | [true_, 'packs/b', 'packs/a/packs/1', true_, 'not siblings or child'] 39 | ].each do |test| 40 | test "if #{test[1]} has enforce_folder_privacy: #{test[0]} than a reference from #{test[2]} is #{test[3] ? 'A VIOLATION' : 'OK'}" do 41 | source_package = Packwerk::Package.new(name: test[2]) 42 | destination_package = Packwerk::Package.new(name: test[1], config: { 'enforce_folder_privacy' => test[0] }) 43 | reference = build_reference( 44 | source_package: source_package, 45 | destination_package: destination_package 46 | ) 47 | 48 | assert_equal test[3], folder_privacy_checker.invalid_reference?(reference) 49 | end 50 | end 51 | 52 | test 'provides a useful message' do 53 | assert_equal folder_privacy_checker.message(build_reference), <<~MSG.chomp 54 | Folder Privacy violation: '::SomeName' belongs to 'components/destination', which is private to 'components/source' as it is not a sibling pack or parent pack. 55 | Is there a different package to use instead, or should 'components/destination' also be visible to 'components/source'? 56 | 57 | Inference details: this is a reference to ::SomeName which seems to be defined in some/location.rb. 58 | To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations 59 | MSG 60 | end 61 | 62 | private 63 | 64 | sig { returns(Checker) } 65 | def folder_privacy_checker 66 | FolderPrivacy::Checker.new 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/unit/folder_privacy/validator_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | # make sure PrivateThing.constantize succeeds to pass the privacy validity check 7 | require 'fixtures/skeleton/components/timeline/app/models/private_thing' 8 | 9 | module Packwerk 10 | module FolderPrivacy 11 | class ValidatorTest < Minitest::Test 12 | extend T::Sig 13 | include ApplicationFixtureHelper 14 | include RailsApplicationFixtureHelper 15 | 16 | setup do 17 | setup_application_fixture 18 | end 19 | 20 | teardown do 21 | teardown_application_fixture 22 | end 23 | 24 | # test 'call returns an error for invalid enforce_visibility value' do 25 | # use_template(:minimal) 26 | # merge_into_app_yaml_file('package.yml', { 'enforce_folder_privacy' => 'yes, please.' }) 27 | 28 | # result = validator.call(package_set, config) 29 | 30 | # refute result.ok? 31 | # assert_match(/Invalid 'enforce_folder_privacy' option/, result.error_value) 32 | # end 33 | 34 | test 'call returns success when enforce_folder_privacy is set to strict' do 35 | use_template(:minimal) 36 | merge_into_app_yaml_file('package.yml', { 'enforce_folder_privacy' => 'strict' }) 37 | 38 | result = validator.call(package_set, config) 39 | 40 | assert result.ok? 41 | end 42 | 43 | sig { returns(Packwerk::FolderPrivacy::Validator) } 44 | def validator 45 | @validator ||= Packwerk::FolderPrivacy::Validator.new 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/unit/layer/checker_architecture_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | module Packwerk 7 | module Layer 8 | class CheckerTest < Minitest::Test 9 | extend T::Sig 10 | include FactoryHelper 11 | include RailsApplicationFixtureHelper 12 | 13 | def write_config 14 | write_app_file('packwerk.yml', <<~YML) 15 | architecture_layers: 16 | - orchestrator 17 | - business_domain 18 | - platform 19 | - utility 20 | - specification 21 | YML 22 | end 23 | 24 | def orchestrator_pack(enforce: false) 25 | Packwerk::Package.new(name: 'packs/orchestrator', config: { 'enforce_layers' => enforce, 'layer' => 'orchestrator' }) 26 | end 27 | 28 | def utility_pack(enforce: false) 29 | Packwerk::Package.new(name: 'packs/utility', config: { 'enforce_layers' => enforce, 'layer' => 'utility' }) 30 | end 31 | 32 | setup do 33 | setup_application_fixture 34 | use_template(:minimal) 35 | write_config 36 | end 37 | 38 | teardown do 39 | teardown_application_fixture 40 | end 41 | 42 | test 'ignores if origin package is not enforcing' do 43 | checker = layer_checker 44 | reference = build_reference( 45 | source_package: utility_pack(enforce: false), 46 | destination_package: orchestrator_pack(enforce: false) 47 | ) 48 | 49 | refute checker.invalid_reference?(reference) 50 | end 51 | 52 | test 'is an invalid reference if destination pack is above source package' do 53 | checker = layer_checker 54 | reference = build_reference( 55 | source_package: utility_pack(enforce: true), 56 | destination_package: orchestrator_pack(enforce: false) 57 | ) 58 | 59 | assert checker.invalid_reference?(reference) 60 | end 61 | 62 | test 'infers layer based on root directory' do 63 | orchestrator_pack = Packwerk::Package.new(name: 'orchestrator/some_pack', config: { 'enforce_layers' => true }) 64 | utility_pack = Packwerk::Package.new(name: 'utility/some_other_pack', config: { 'enforce_layers' => true }) 65 | checker = layer_checker 66 | reference = build_reference( 67 | source_package: utility_pack, 68 | destination_package: orchestrator_pack 69 | ) 70 | 71 | assert_equal Package.from(orchestrator_pack, Layers.new).layer, 'orchestrator' 72 | assert_equal Package.from(utility_pack, Layers.new).layer, 'utility' 73 | assert checker.invalid_reference?(reference) 74 | end 75 | 76 | test 'allows layer setting to override root directory location' do 77 | orchestrator_pack = Packwerk::Package.new(name: 'orchestrator/some_pack', config: { 'layer' => 'specification', 'enforce_layers' => true }) 78 | utility_pack = Packwerk::Package.new(name: 'utility/some_other_pack', config: { 'enforce_layers' => true }) 79 | checker = layer_checker 80 | reference = build_reference( 81 | source_package: utility_pack, 82 | destination_package: orchestrator_pack 83 | ) 84 | 85 | assert_equal Package.from(orchestrator_pack, Layers.new).layer, 'specification' 86 | assert_equal Package.from(utility_pack, Layers.new).layer, 'utility' 87 | refute checker.invalid_reference?(reference) 88 | end 89 | 90 | test 'is not an invalid reference if destination pack is below source package' do 91 | checker = layer_checker 92 | reference = build_reference( 93 | source_package: orchestrator_pack(enforce: true), 94 | destination_package: utility_pack(enforce: false) 95 | ) 96 | 97 | refute checker.invalid_reference?(reference) 98 | end 99 | 100 | test 'provides a useful message' do 101 | reference = build_reference( 102 | source_package: utility_pack(enforce: true), 103 | destination_package: orchestrator_pack(enforce: false) 104 | ) 105 | 106 | assert_equal layer_checker.message(reference), <<~MSG.chomp 107 | Layer violation: '::SomeName' belongs to 'packs/orchestrator', whose layer type is "orchestrator". 108 | This constant cannot be referenced by 'packs/utility', whose layer type is "utility". 109 | Packs in a lower layer may not access packs in a higher layer. See the `layers` in packwerk.yml. Current hierarchy: 110 | - orchestrator 111 | - business_domain 112 | - platform 113 | - utility 114 | - specification 115 | 116 | Inference details: this is a reference to ::SomeName which seems to be defined in some/location.rb. 117 | To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations 118 | MSG 119 | end 120 | 121 | private 122 | 123 | sig { returns(Checker) } 124 | def layer_checker 125 | Packwerk::Layer::Checker.new 126 | end 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /test/unit/layer/checker_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | module Packwerk 7 | module Layer 8 | class CheckerTest < Minitest::Test 9 | extend T::Sig 10 | include FactoryHelper 11 | include RailsApplicationFixtureHelper 12 | 13 | def write_config 14 | write_app_file('packwerk.yml', <<~YML) 15 | layers: 16 | - orchestrator 17 | - business_domain 18 | - platform 19 | - utility 20 | - specification 21 | YML 22 | end 23 | 24 | def orchestrator_pack(enforce: false) 25 | Packwerk::Package.new(name: 'packs/orchestrator', config: { 'enforce_layers' => enforce, 'layer' => 'orchestrator' }) 26 | end 27 | 28 | def utility_pack(enforce: false) 29 | Packwerk::Package.new(name: 'packs/utility', config: { 'enforce_layers' => enforce, 'layer' => 'utility' }) 30 | end 31 | 32 | setup do 33 | setup_application_fixture 34 | use_template(:minimal) 35 | write_config 36 | end 37 | 38 | teardown do 39 | teardown_application_fixture 40 | end 41 | 42 | test 'determines violation type' do 43 | assert_equal layer_checker.violation_type, 'layer' 44 | end 45 | 46 | test 'ignores if origin package is not enforcing' do 47 | checker = layer_checker 48 | reference = build_reference( 49 | source_package: utility_pack(enforce: false), 50 | destination_package: orchestrator_pack(enforce: false) 51 | ) 52 | 53 | refute checker.invalid_reference?(reference) 54 | end 55 | 56 | test 'is an invalid reference if destination pack is above source package' do 57 | checker = layer_checker 58 | reference = build_reference( 59 | source_package: utility_pack(enforce: true), 60 | destination_package: orchestrator_pack(enforce: false) 61 | ) 62 | 63 | assert checker.invalid_reference?(reference) 64 | end 65 | 66 | test 'infers layer based on root directory' do 67 | orchestrator_pack = Packwerk::Package.new(name: 'orchestrator/some_pack', config: { 'enforce_layers' => true }) 68 | utility_pack = Packwerk::Package.new(name: 'utility/some_other_pack', config: { 'enforce_layers' => true }) 69 | checker = layer_checker 70 | reference = build_reference( 71 | source_package: utility_pack, 72 | destination_package: orchestrator_pack 73 | ) 74 | 75 | assert_equal Package.from(orchestrator_pack, Layers.new).layer, 'orchestrator' 76 | assert_equal Package.from(utility_pack, Layers.new).layer, 'utility' 77 | assert checker.invalid_reference?(reference) 78 | end 79 | 80 | test 'allows layer setting to override root directory location' do 81 | orchestrator_pack = Packwerk::Package.new(name: 'orchestrator/some_pack', config: { 'layer' => 'specification', 'enforce_layers' => true }) 82 | utility_pack = Packwerk::Package.new(name: 'utility/some_other_pack', config: { 'enforce_layers' => true }) 83 | checker = layer_checker 84 | reference = build_reference( 85 | source_package: utility_pack, 86 | destination_package: orchestrator_pack 87 | ) 88 | 89 | assert_equal Package.from(orchestrator_pack, Layers.new).layer, 'specification' 90 | assert_equal Package.from(utility_pack, Layers.new).layer, 'utility' 91 | refute checker.invalid_reference?(reference) 92 | end 93 | 94 | test 'is not an invalid reference if destination pack is below source package' do 95 | checker = layer_checker 96 | reference = build_reference( 97 | source_package: orchestrator_pack(enforce: true), 98 | destination_package: utility_pack(enforce: false) 99 | ) 100 | 101 | refute checker.invalid_reference?(reference) 102 | end 103 | 104 | test 'provides a useful message' do 105 | reference = build_reference( 106 | source_package: utility_pack(enforce: true), 107 | destination_package: orchestrator_pack(enforce: false) 108 | ) 109 | 110 | assert_equal layer_checker.message(reference), <<~MSG.chomp 111 | Layer violation: '::SomeName' belongs to 'packs/orchestrator', whose layer type is "orchestrator". 112 | This constant cannot be referenced by 'packs/utility', whose layer type is "utility". 113 | Packs in a lower layer may not access packs in a higher layer. See the `layers` in packwerk.yml. Current hierarchy: 114 | - orchestrator 115 | - business_domain 116 | - platform 117 | - utility 118 | - specification 119 | 120 | Inference details: this is a reference to ::SomeName which seems to be defined in some/location.rb. 121 | To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations 122 | MSG 123 | end 124 | 125 | private 126 | 127 | sig { returns(Checker) } 128 | def layer_checker 129 | Packwerk::Layer::Checker.new 130 | end 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /test/unit/layer/config_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | module Packwerk 7 | module Layer 8 | class ConfigTest < Minitest::Test 9 | extend T::Sig 10 | include FactoryHelper 11 | include RailsApplicationFixtureHelper 12 | 13 | def write_config 14 | write_app_file('packwerk.yml', <<~YML) 15 | layers: 16 | - orchestrator 17 | - business_domain 18 | YML 19 | end 20 | 21 | def write_architecture_config 22 | write_app_file('packwerk.yml', <<~YML) 23 | architecture_layers: 24 | - orchestrator 25 | - business_domain 26 | YML 27 | end 28 | 29 | setup do 30 | setup_application_fixture 31 | use_template(:minimal) 32 | write_config 33 | end 34 | 35 | teardown do 36 | teardown_application_fixture 37 | end 38 | 39 | test 'determines correct keys' do 40 | assert config_instance.layers_key_configured? 41 | assert_equal config_instance.layers_key, 'layers' 42 | assert_equal config_instance.violation_key, 'layer' 43 | assert_equal config_instance.enforce_key, 'enforce_layers' 44 | end 45 | 46 | test 'finds the layers' do 47 | assert_equal config_instance.layers_list, %w[orchestrator business_domain] 48 | end 49 | 50 | test 'determines architecture keys' do 51 | write_architecture_config 52 | refute config_instance.layers_key_configured? 53 | assert_equal config_instance.layers_key, 'architecture_layers' 54 | assert_equal config_instance.violation_key, 'architecture' 55 | assert_equal config_instance.enforce_key, 'enforce_architecture' 56 | assert_equal config_instance.layers_list, %w[orchestrator business_domain] 57 | end 58 | 59 | private 60 | 61 | sig { returns(Config) } 62 | def config_instance 63 | Packwerk::Layer::Config.new 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/unit/layer/layers_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | module Packwerk 7 | module Layer 8 | class LayersTest < Minitest::Test 9 | extend T::Sig 10 | include FactoryHelper 11 | include RailsApplicationFixtureHelper 12 | 13 | def write_config 14 | write_app_file('packwerk.yml', <<~YML) 15 | layers: 16 | - orchestrator 17 | - business_domain 18 | YML 19 | end 20 | 21 | setup do 22 | setup_application_fixture 23 | use_template(:minimal) 24 | write_config 25 | end 26 | 27 | teardown do 28 | teardown_application_fixture 29 | end 30 | 31 | test 'finds the configured layers' do 32 | assert_equal layers_class.names, Set.new(%w[orchestrator business_domain]) 33 | end 34 | 35 | private 36 | 37 | sig { returns(Layers) } 38 | def layers_class 39 | Packwerk::Layer::Layers.new 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/unit/layer/validator_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | module Packwerk 7 | module Layer 8 | class ValidatorTest < Minitest::Test 9 | extend T::Sig 10 | include ApplicationFixtureHelper 11 | include RailsApplicationFixtureHelper 12 | 13 | def write_config 14 | write_app_file('packwerk.yml', <<~YML) 15 | layers: 16 | - package 17 | - utility 18 | YML 19 | end 20 | 21 | def write_architecture_config 22 | write_config 23 | write_app_file('packwerk.yml', <<~YML) 24 | architecture_layers: 25 | - package 26 | - utility 27 | YML 28 | end 29 | 30 | setup do 31 | setup_application_fixture 32 | use_template(:minimal) 33 | write_config 34 | end 35 | 36 | teardown do 37 | teardown_application_fixture 38 | end 39 | 40 | test 'call returns no error if enforce_layers is unset' do 41 | write_app_file('packwerk.yml', <<~YML) 42 | {} 43 | YML 44 | result = validator.call(package_set, config) 45 | 46 | assert result.ok? 47 | end 48 | 49 | test 'call returns an error for invalid enforce_layers value' do 50 | merge_into_app_yaml_file('package.yml', { 'enforce_layers' => 'yes, please.' }) 51 | 52 | result = validator.call(package_set, config) 53 | 54 | refute result.ok? 55 | assert_match(/Invalid 'enforce_layers' option/, result.error_value) 56 | end 57 | 58 | test 'call returns an error for invalid enforce_architecture value' do 59 | write_architecture_config 60 | 61 | merge_into_app_yaml_file('package.yml', { 'enforce_architecture' => 'yes, please.' }) 62 | 63 | result = validator.call(package_set, config) 64 | 65 | refute result.ok? 66 | assert_match(/Invalid 'enforce_architecture' option/, result.error_value) 67 | end 68 | 69 | test 'call returns success when enforce_layers is set to strict' do 70 | merge_into_app_yaml_file('package.yml', { 'enforce_layers' => 'strict', 'layer' => 'utility' }) 71 | 72 | result = validator.call(package_set, config) 73 | assert result.ok? 74 | end 75 | 76 | test 'call returns success when enforce_architecture is set to strict' do 77 | write_architecture_config 78 | 79 | merge_into_app_yaml_file('package.yml', { 'enforce_architecture' => 'strict', 'layer' => 'utility' }) 80 | 81 | result = validator.call(package_set, config) 82 | assert result.ok? 83 | end 84 | 85 | test 'call returns error when enforce_layers is set to strict, but enforce_architecture is required' do 86 | write_architecture_config 87 | 88 | merge_into_app_yaml_file('package.yml', { 'enforce_layers' => 'strict', 'layer' => 'utility' }) 89 | 90 | result = validator.call(package_set, config) 91 | refute result.ok? 92 | 93 | assert_match(/Unexpected `enforce_layers` option in/, result.error_value) 94 | end 95 | 96 | test 'call returns error when enforce_architecture is set to strict, but enforce_layers is required' do 97 | merge_into_app_yaml_file('package.yml', { 'enforce_architecture' => 'strict', 'layer' => 'utility' }) 98 | 99 | result = validator.call(package_set, config) 100 | refute result.ok? 101 | 102 | assert_match(/Unexpected `enforce_architecture` option in/, result.error_value) 103 | end 104 | 105 | test 'call returns an error for invalid layer value' do 106 | merge_into_app_yaml_file('package.yml', { 'layer' => 'blah' }) 107 | 108 | result = validator.call(package_set, config) 109 | 110 | refute result.ok? 111 | assert_match(/Invalid 'layer' option in.*?package.yml": "blah". Must be one of \["package", "utility"\]/, result.error_value) 112 | end 113 | 114 | # We return no error here because it's possible we want to set layers for things so consuming packages can enforce their layer 115 | # without the publishing package needing to enforce it. 116 | test 'call returns no error if a layer is set with enforce_layers not on' do 117 | merge_into_app_yaml_file('package.yml', { 'layer' => 'utility' }) 118 | 119 | result = validator.call(package_set, config) 120 | assert result.ok? 121 | end 122 | 123 | test 'call returns an error if a layer is unset with enforce_layers on' do 124 | merge_into_app_yaml_file('package.yml', { 'enforce_layers' => true }) 125 | 126 | result = validator.call(package_set, config) 127 | 128 | refute result.ok? 129 | assert_match(/Invalid 'layer' option in.*?package.yml": nil. `layer` must be set if `enforce_layers` is on./, result.error_value) 130 | end 131 | 132 | test 'call returns an error if a layer is unset with enforce_architecture on' do 133 | write_architecture_config 134 | 135 | merge_into_app_yaml_file('package.yml', { 'enforce_architecture' => true }) 136 | 137 | result = validator.call(package_set, config) 138 | 139 | refute result.ok? 140 | assert_match(/Invalid 'layer' option in.*?package.yml": nil. `layer` must be set if `enforce_architecture` is on./, result.error_value) 141 | end 142 | 143 | test 'call returns an error if enforce_layers is set without layers specified' do 144 | write_app_file('packwerk.yml', <<~YML) 145 | {} 146 | YML 147 | merge_into_app_yaml_file('package.yml', { 'enforce_layers' => true }) 148 | 149 | result = validator.call(package_set, config) 150 | 151 | refute result.ok? 152 | 153 | assert_match(/Cannot set 'enforce_layers' option in.*?package.yml" until `layers` have been specified in `packwerk.yml`/, result.error_value) 154 | end 155 | 156 | test 'call returns no error for valid layer value' do 157 | merge_into_app_yaml_file('package.yml', { 'enforce_layers' => true, 'layer' => 'utility' }) 158 | 159 | result = validator.call(package_set, config) 160 | assert result.ok? 161 | end 162 | 163 | test 'call returns no error for no layer value if layer is implied by root location' do 164 | merge_into_app_yaml_file('utility/package.yml', { 'enforce_layers' => true }) 165 | result = validator.call(package_set, config) 166 | assert result.ok? 167 | end 168 | 169 | test 'call permitted keys' do 170 | assert_equal validator.permitted_keys, %w[enforce_layers layer] 171 | end 172 | 173 | test 'call permitted keys when architecture' do 174 | write_architecture_config 175 | assert_equal validator.permitted_keys, %w[enforce_architecture layer] 176 | end 177 | 178 | sig { returns(Packwerk::Layer::Validator) } 179 | def validator 180 | @validator ||= Packwerk::Layer::Validator.new 181 | end 182 | end 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /test/unit/privacy/checker_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | module Packwerk 7 | module Privacy 8 | class CheckerTest < Minitest::Test 9 | extend T::Sig 10 | include FactoryHelper 11 | include RailsApplicationFixtureHelper 12 | 13 | setup do 14 | setup_application_fixture 15 | end 16 | 17 | teardown do 18 | teardown_application_fixture 19 | end 20 | 21 | test 'ignores if destination package is not enforcing' do 22 | destination_package = Packwerk::Package.new( 23 | name: 'destination_package', 24 | config: { 'enforce_privacy' => false, 'private_constants' => ['::SomeName'] } 25 | ) 26 | checker = privacy_checker 27 | reference = build_reference(destination_package: destination_package) 28 | 29 | refute checker.invalid_reference?(reference) 30 | end 31 | 32 | test 'ignores if destination package is only enforcing for other constants' do 33 | destination_package = Packwerk::Package.new( 34 | name: 'destination_package', 35 | config: { 'enforce_privacy' => true, 'private_constants' => ['::SomeOtherConstant'] } 36 | ) 37 | checker = privacy_checker 38 | reference = build_reference(destination_package: destination_package) 39 | 40 | refute checker.invalid_reference?(reference) 41 | end 42 | 43 | test 'complains about private constant if enforcing privacy for everything' do 44 | destination_package = Packwerk::Package.new(name: 'destination_package', config: { 'enforce_privacy' => true }) 45 | checker = privacy_checker 46 | reference = build_reference(destination_package: destination_package) 47 | 48 | assert checker.invalid_reference?(reference) 49 | end 50 | 51 | test 'does not complain about private constant if enforcing privacy for everything and the destination is publicizing the file' do 52 | destination_package = Packwerk::Package.new(name: 'destination_package', config: { 'enforce_privacy' => true }) 53 | checker = privacy_checker 54 | reference = build_reference(destination_package: destination_package) 55 | Packwerk::Privacy::Checker.publicized_locations['some/location.rb'] = true 56 | refute checker.invalid_reference?(reference) 57 | end 58 | 59 | test 'does not complain about private constant if it is an ignored_private_constant when using enforce_privacy' do 60 | destination_package = Packwerk::Package.new(name: 'destination_package', config: { 'ignored_private_constants' => ['::SomeName'], 'enforce_privacy' => true }) 61 | checker = privacy_checker 62 | reference = build_reference(destination_package: destination_package) 63 | 64 | refute checker.invalid_reference?(reference) 65 | end 66 | 67 | test 'complains about private constant if enforcing for specific constants' do 68 | destination_package = Packwerk::Package.new(name: 'destination_package', config: { 'enforce_privacy' => true, 'private_constants' => ['::SomeName'] }) 69 | checker = privacy_checker 70 | reference = build_reference(destination_package: destination_package) 71 | 72 | assert checker.invalid_reference?(reference) 73 | end 74 | 75 | test 'complains about nested constant if enforcing for specific constants' do 76 | destination_package = Packwerk::Package.new(name: 'destination_package', config: { 'enforce_privacy' => true, 'private_constants' => ['::SomeName'] }) 77 | checker = privacy_checker 78 | reference = build_reference(destination_package: destination_package, constant_name: '::SomeName::Nested') 79 | 80 | assert checker.invalid_reference?(reference) 81 | end 82 | 83 | test 'ignores constant that starts like enforced constant' do 84 | destination_package = Packwerk::Package.new(name: 'destination_package', config: { 'enforce_privacy' => true, 'private_constants' => ['::SomeName'] }) 85 | checker = privacy_checker 86 | reference = build_reference(destination_package: destination_package, constant_name: '::SomeNameButNotQuite') 87 | 88 | refute checker.invalid_reference?(reference) 89 | end 90 | 91 | test 'ignores public constant even if enforcing privacy for everything' do 92 | destination_package = Packwerk::Package.new(name: 'destination_package', config: { 'enforce_privacy' => true }) 93 | checker = privacy_checker 94 | reference = build_reference(destination_package: destination_package, constant_location: 'destination_package/app/public/') 95 | 96 | refute checker.invalid_reference?(reference) 97 | end 98 | 99 | test 'ignores strict mode if not enabled' do 100 | destination_package = Packwerk::Package.new(name: 'destination_package', config: { 'enforce_privacy' => true }) 101 | checker = privacy_checker 102 | reference = build_reference(destination_package: destination_package, constant_location: 'destination_package/app/public/') 103 | offense = Packwerk::ReferenceOffense.new(reference: reference, violation_type: 'privacy', message: '') 104 | 105 | refute checker.strict_mode_violation?(offense) 106 | end 107 | 108 | test 'detect strict mode if enabled' do 109 | destination_package = Packwerk::Package.new(name: 'destination_package', config: { 'enforce_privacy' => 'strict' }) 110 | checker = privacy_checker 111 | reference = build_reference(destination_package: destination_package, constant_location: 'destination_package/app/public/') 112 | offense = Packwerk::ReferenceOffense.new(reference: reference, violation_type: 'privacy', message: '') 113 | 114 | assert checker.strict_mode_violation?(offense) 115 | end 116 | 117 | test 'ignores strict mode if excluded path' do 118 | destination_package = Packwerk::Package.new(name: 'destination_package', config: { 'enforce_privacy' => 'strict', 'strict_privacy_ignored_patterns' => ['some/**'] }) 119 | checker = privacy_checker 120 | reference = build_reference(destination_package: destination_package, constant_location: 'destination_package/app/public/') 121 | offense = Packwerk::ReferenceOffense.new(reference: reference, violation_type: 'privacy', message: '') 122 | 123 | refute checker.strict_mode_violation?(offense) 124 | end 125 | 126 | test 'detects strict mode if not excluded path' do 127 | destination_package = Packwerk::Package.new(name: 'destination_package', config: { 'enforce_privacy' => 'strict', 'strict_privacy_ignored_patterns' => ['test/**'] }) 128 | checker = privacy_checker 129 | reference = build_reference(destination_package: destination_package, constant_location: 'destination_package/app/public/') 130 | offense = Packwerk::ReferenceOffense.new(reference: reference, violation_type: 'privacy', message: '') 131 | 132 | assert checker.strict_mode_violation?(offense) 133 | end 134 | 135 | test 'only checks the package TODO file for private constants' do 136 | destination_package = Packwerk::Package.new(name: 'destination_package', config: { 'enforce_privacy' => true, 'private_constants' => ['::SomeName'] }) 137 | checker = privacy_checker 138 | reference = build_reference(destination_package: destination_package) 139 | 140 | checker.invalid_reference?(reference) 141 | end 142 | 143 | test 'provides a useful message' do 144 | assert_equal privacy_checker.message(build_reference), <<~MSG.chomp 145 | Privacy violation: '::SomeName' is private to 'components/destination' but referenced from 'components/source'. 146 | Is there a public entrypoint in 'components/destination/app/public/' that you can use instead? 147 | 148 | Inference details: this is a reference to ::SomeName which seems to be defined in some/location.rb. 149 | To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations 150 | MSG 151 | end 152 | 153 | test 'content_contains_sigil?' do 154 | content_with_valid_sigils = [ 155 | ['line 1', 'line 2', 'line 3', 'line 4', '# pack_public: true'], 156 | ['#pack_public:true', 'line 2', 'line 3'], 157 | ['line 1', '# pack_public: true'] 158 | ] 159 | content_with_invalid_or_missing_sigils = [ 160 | ['line 1', 'line 2', 'line 3', 'line 4', 'line 5', '# pack_public: true'], 161 | ['#pulic_api:', 'line 2', 'line 3'], 162 | ['line 1', '# pack_public: false'], 163 | ['# pack_public: false', 'line 2', 'line 3'], 164 | ['line 1', 'EOF'] 165 | ] 166 | assert(content_with_valid_sigils.all? { |content| Privacy::Checker.content_contains_sigil?(content) }) 167 | assert(content_with_invalid_or_missing_sigils.none? { |content| Privacy::Checker.content_contains_sigil?(content) }) 168 | end 169 | 170 | private 171 | 172 | sig { returns(Checker) } 173 | def privacy_checker 174 | Privacy::Checker.new 175 | end 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /test/unit/privacy/package_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | module Packwerk 7 | module Privacy 8 | class PackageTest < Minitest::Test 9 | extend T::Sig 10 | 11 | include RailsApplicationFixtureHelper 12 | 13 | setup do 14 | setup_application_fixture 15 | @package = Packwerk::Package.new(name: 'components/timeline', config: { 'enforce_privacy' => true, 'private_constants' => ['::Test'] }) 16 | end 17 | 18 | teardown do 19 | teardown_application_fixture 20 | end 21 | 22 | sig { returns(Package) } 23 | def privacy_package 24 | Package.from(@package) 25 | end 26 | 27 | test '#enforce_privacy returns same value as from config' do 28 | assert_equal(true, privacy_package.enforce_privacy) 29 | end 30 | 31 | test '#public_path returns expected path when using the default public path' do 32 | assert_equal('components/timeline/app/public/', privacy_package.public_path) 33 | end 34 | 35 | test '#public_path returns expected path when using a user defined public path' do 36 | @package = Packwerk::Package.new(name: 'components/timeline', config: { 'public_path' => ['my/path'] }) 37 | assert_equal('components/timeline/my/path/', privacy_package.public_path) 38 | end 39 | 40 | test '#public_path returns expected path when using the default public path in root package' do 41 | @package = Packwerk::Package.new(name: '.', config: {}) 42 | assert_equal('app/public/', privacy_package.public_path) 43 | end 44 | 45 | test '#public_path returns expected path when using a user defined public path' do 46 | @package = Packwerk::Package.new(name: '.', config: { 'public_path' => 'my/path/' }) 47 | 48 | assert_equal('my/path/', privacy_package.public_path) 49 | end 50 | 51 | test "#public_path? returns true for path under the root package's public path" do 52 | @package = Packwerk::Package.new(name: '.', config: {}) 53 | assert_equal(true, privacy_package.public_path?('app/public/entrypoint.rb')) 54 | end 55 | 56 | test "#public_path? returns false for path not under the root package's public path" do 57 | @package = Packwerk::Package.new(name: '.', config: {}) 58 | assert_equal(false, privacy_package.public_path?('app/models/something.rb')) 59 | end 60 | 61 | test '#user_defined_public_path returns the same value as in the config when set' do 62 | @package = Packwerk::Package.new(name: 'components/timeline', config: { 'public_path' => 'my/path/' }) 63 | assert_equal('my/path/', privacy_package.user_defined_public_path) 64 | end 65 | 66 | test '#user_defined_public_path adds a trailing forward slash to the path if it does not exist' do 67 | @package = Packwerk::Package.new(name: 'components/timeline', config: { 'public_path' => 'my/path' }) 68 | assert_equal('my/path/', privacy_package.user_defined_public_path) 69 | end 70 | 71 | test "#public_path? returns true for path under the package's public path" do 72 | assert_equal(true, privacy_package.public_path?('components/timeline/app/public/entrypoint.rb')) 73 | end 74 | 75 | test "#public_path? returns false for path not under the package's public path" do 76 | assert_equal(false, privacy_package.public_path?('components/timeline/app/models/something.rb')) 77 | end 78 | 79 | test '#user_defined_public_path returns nil when not set in the configuration' do 80 | assert_nil(privacy_package.user_defined_public_path) 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/unit/privacy/validator_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | # make sure PrivateThing.constantize succeeds to pass the privacy validity check 7 | require 'fixtures/skeleton/components/timeline/app/models/private_thing' 8 | 9 | module Packwerk 10 | module Privacy 11 | class ValidatorTest < Minitest::Test 12 | extend T::Sig 13 | include ApplicationFixtureHelper 14 | include RailsApplicationFixtureHelper 15 | 16 | setup do 17 | setup_application_fixture 18 | end 19 | 20 | teardown do 21 | teardown_application_fixture 22 | end 23 | 24 | test 'check_all returns an error for invalid enforce_privacy value' do 25 | use_template(:minimal) 26 | merge_into_app_yaml_file('package.yml', { 'enforce_privacy' => 'yes, please.' }) 27 | 28 | result = Packwerk::Privacy::Validator.new.call(package_set, config) 29 | 30 | refute result.ok? 31 | assert_match(/Invalid 'enforce_privacy' option/, result.error_value) 32 | end 33 | 34 | test 'check_all returns success for when enforce_privacy is set to strict' do 35 | use_template(:minimal) 36 | merge_into_app_yaml_file('package.yml', { 'enforce_privacy' => 'strict' }) 37 | 38 | result = Packwerk::Privacy::Validator.new.call(package_set, config) 39 | 40 | assert result.ok? 41 | end 42 | 43 | test 'check_all returns an error when a privatized constant is declared and the constant is also declaring pack_public: true' do 44 | use_template(:skeleton) 45 | merge_into_app_yaml_file('/components/sales/package.yml', { 'private_constants' => ['::Order::Foo'] }) 46 | result = Packwerk::Privacy::Validator.new.call(package_set, config) 47 | refute result.ok? 48 | assert_match(/is an explicitly publicized constant declared in/, result.error_value) 49 | end 50 | 51 | test 'check_all returns success when inflector defines acronym' do 52 | use_template(:skeleton) 53 | 54 | result = Packwerk::Privacy::Validator.new.call(package_set, config) 55 | 56 | assert result.ok? 57 | assert_nil result.error_value 58 | end 59 | 60 | test 'check_all returns an error for invalid public_path value' do 61 | use_template(:minimal) 62 | merge_into_app_yaml_file('package.yml', { 'public_path' => [] }) 63 | 64 | result = Packwerk::Privacy::Validator.new.call(package_set, config) 65 | 66 | refute result.ok? 67 | assert_match(/'public_path' option must be a string/, result.error_value) 68 | end 69 | 70 | test 'check_package_manifests_for_privacy returns an error for unresolvable privatized constants' do 71 | use_template(:skeleton) 72 | ConstantResolver.expects(:new).returns(stub('resolver', resolve: nil)) 73 | 74 | result = Packwerk::Privacy::Validator.new.call(package_set, config) 75 | refute result.ok?, result.error_value 76 | assert_match( 77 | /'::PrivateThing', listed in #{to_app_path('components\/timeline\/package.yml')}, could not be resolved/, 78 | result.error_value 79 | ) 80 | assert_match( 81 | /Add a private_thing.rb file/, 82 | result.error_value 83 | ) 84 | end 85 | 86 | test 'check_package_manifests_for_privacy returns an error for privatized constants in other packages' do 87 | use_template(:skeleton) 88 | context = ConstantResolver::ConstantContext.new('::PrivateThing', 'private_thing.rb') 89 | 90 | ConstantResolver.expects(:new).returns(stub('resolver', resolve: context)) 91 | 92 | result = Packwerk::Privacy::Validator.new.call(package_set, config) 93 | 94 | refute result.ok?, result.error_value 95 | assert_match( 96 | %r{'::PrivateThing' is declared as private in the 'components/timeline' package}, 97 | result.error_value 98 | ) 99 | assert_match( 100 | /but appears to be defined\sin the '.' package/, 101 | result.error_value 102 | ) 103 | end 104 | 105 | test 'check_package_manifests_for_privacy returns an error for constants without `::` prefix' do 106 | use_template(:minimal) 107 | merge_into_app_yaml_file('package.yml', { 'private_constants' => ['::PrivateThing', 'OtherThing'] }) 108 | 109 | result = Packwerk::Privacy::Validator.new.call(package_set, config) 110 | 111 | refute result.ok?, result.error_value 112 | assert_match( 113 | /'OtherThing', listed in the 'private_constants' option in .*package.yml, is invalid./, 114 | result.error_value 115 | ) 116 | assert_match( 117 | /Private constants need to be prefixed with the top-level namespace operator `::`/, 118 | result.error_value 119 | ) 120 | end 121 | 122 | test 'does not create a validation error when using ignored_private_constants' do 123 | use_template(:minimal) 124 | merge_into_app_yaml_file('package.yml', { 'ignored_private_constants' => ['packs/my_other_pack'] }) 125 | result = validator.call(package_set, config) 126 | assert result.ok? 127 | end 128 | 129 | private 130 | 131 | sig { returns(ApplicationValidator) } 132 | def validator 133 | @validator ||= ApplicationValidator.new 134 | end 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /test/unit/visibility/checker_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | module Packwerk 7 | module Visibility 8 | class CheckerTest < Minitest::Test 9 | extend T::Sig 10 | include FactoryHelper 11 | include RailsApplicationFixtureHelper 12 | 13 | setup do 14 | setup_application_fixture 15 | end 16 | 17 | teardown do 18 | teardown_application_fixture 19 | end 20 | 21 | test 'ignores if destination package is not enforcing' do 22 | checker = visibility_checker 23 | reference = build_reference 24 | 25 | refute checker.invalid_reference?(reference) 26 | end 27 | 28 | test 'is an invalid reference if destination pack is not visible to reference pack' do 29 | checker = visibility_checker 30 | destination_package = Packwerk::Package.new( 31 | name: 'destination_package', 32 | config: { 'enforce_visibility' => true, visible_to: 'other_pack' } 33 | ) 34 | reference = build_reference(destination_package: destination_package) 35 | 36 | assert checker.invalid_reference?(reference) 37 | end 38 | 39 | test 'is not an invalid reference if destination pack is visible to reference pack' do 40 | checker = visibility_checker 41 | destination_package = Packwerk::Package.new( 42 | name: 'destination_package', 43 | config: { 'enforce_visibility' => true, 'visible_to' => ['components/source'] } 44 | ) 45 | reference = build_reference(destination_package: destination_package) 46 | 47 | refute checker.invalid_reference?(reference) 48 | end 49 | 50 | test 'provides a useful message' do 51 | assert_equal visibility_checker.message(build_reference), <<~MSG.chomp 52 | Visibility violation: '::SomeName' belongs to 'components/destination', which is not visible to 'components/source'. 53 | Is there a different package to use instead, or should 'components/destination' also be visible to 'components/source'? 54 | 55 | Inference details: this is a reference to ::SomeName which seems to be defined in some/location.rb. 56 | To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations 57 | MSG 58 | end 59 | 60 | private 61 | 62 | sig { returns(Checker) } 63 | def visibility_checker 64 | Visibility::Checker.new 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/unit/visibility/validator_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | require 'test_helper' 5 | 6 | # make sure PrivateThing.constantize succeeds to pass the privacy validity check 7 | require 'fixtures/skeleton/components/timeline/app/models/private_thing' 8 | 9 | module Packwerk 10 | module Visibility 11 | class ValidatorTest < Minitest::Test 12 | extend T::Sig 13 | include ApplicationFixtureHelper 14 | include RailsApplicationFixtureHelper 15 | 16 | setup do 17 | setup_application_fixture 18 | end 19 | 20 | teardown do 21 | teardown_application_fixture 22 | end 23 | 24 | # test 'call returns an error for invalid enforce_visibility value' do 25 | # use_template(:minimal) 26 | # merge_into_app_yaml_file('package.yml', { 'enforce_visibility' => 'yes, please.' }) 27 | 28 | # result = validator.call(package_set, config) 29 | 30 | # refute result.ok? 31 | # assert_match(/Invalid 'enforce_visibility' option/, result.error_value) 32 | # end 33 | 34 | test 'call returns success when enforce_visibility is set to strict' do 35 | use_template(:minimal) 36 | merge_into_app_yaml_file('package.yml', { 'enforce_visibility' => 'strict' }) 37 | 38 | result = validator.call(package_set, config) 39 | 40 | assert result.ok? 41 | end 42 | 43 | test 'call returns an error for invalid visible_to value' do 44 | use_template(:minimal) 45 | merge_into_app_yaml_file('package.yml', { 'visible_to' => 'blah' }) 46 | 47 | result = validator.call(package_set, config) 48 | 49 | refute result.ok? 50 | assert_match(/'visible_to' option must be an array/, result.error_value) 51 | end 52 | 53 | test 'call returns an error for invalid packages in visible_to' do 54 | use_template(:minimal) 55 | merge_into_app_yaml_file('package.yml', { 'visible_to' => ['blah'] }) 56 | 57 | result = validator.call(package_set, config) 58 | 59 | refute result.ok? 60 | assert_match(/'visible_to' option must only contain valid packages in.*?package.yml". Invalid packages: \["blah"\]/, result.error_value) 61 | end 62 | 63 | test 'call returns no errors for valid visible_to values' do 64 | use_template(:minimal) 65 | merge_into_app_yaml_file('package.yml', { 'visible_to' => ['.'] }) 66 | 67 | result = validator.call(package_set, config) 68 | 69 | assert result.ok? 70 | end 71 | 72 | sig { returns(Packwerk::Visibility::Validator) } 73 | def validator 74 | @validator ||= Packwerk::Visibility::Validator.new 75 | end 76 | end 77 | end 78 | end 79 | --------------------------------------------------------------------------------