├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── linting.yml │ └── spell_checking.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .yamllint.yml ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── config ├── default.yml └── obsoletion.yml ├── docs ├── antora.yml └── modules │ └── ROOT │ ├── nav.adoc │ └── pages │ ├── cops.adoc │ ├── cops_threadsafety.adoc │ ├── index.adoc │ ├── installation.adoc │ └── usage.adoc ├── gemfiles └── rubocop_1.72.gemfile ├── lib ├── rubocop-thread_safety.rb └── rubocop │ ├── cop │ ├── mixin │ │ └── operation_with_threadsafe_result.rb │ └── thread_safety │ │ ├── class_and_module_attributes.rb │ │ ├── class_instance_variable.rb │ │ ├── dir_chdir.rb │ │ ├── mutable_class_instance_variable.rb │ │ ├── new_thread.rb │ │ └── rack_middleware_instance_variable.rb │ ├── thread_safety.rb │ └── thread_safety │ ├── plugin.rb │ └── version.rb ├── rubocop-thread_safety.gemspec ├── spec ├── license_spec.rb ├── rubocop │ ├── cop │ │ └── thread_safety │ │ │ ├── class_and_module_attributes_spec.rb │ │ │ ├── class_instance_variable_spec.rb │ │ │ ├── dir_chdir_spec.rb │ │ │ ├── mutable_class_instance_variable_spec.rb │ │ │ ├── new_thread_spec.rb │ │ │ └── rack_middleware_instance_variable_spec.rb │ └── thread_safety_spec.rb └── spec_helper.rb └── tasks └── cops_documentation.rake /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | env: 4 | RUBYOPT: --enable=frozen-string-literal --debug=frozen-string-literal 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | 12 | jobs: 13 | confirm_documentation: 14 | runs-on: ubuntu-latest 15 | name: Confirm documentation 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: "3.2" 21 | bundler-cache: true 22 | - run: bundle exec rake documentation_syntax_check confirm_documentation 23 | 24 | test: 25 | runs-on: ubuntu-latest 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4", ruby-head, jruby-9.4] 30 | rubocop_version: ["1.72"] 31 | env: 32 | BUNDLE_GEMFILE: "gemfiles/rubocop_${{ matrix.rubocop_version }}.gemfile" 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Set up Ruby 36 | uses: ruby/setup-ruby@v1 37 | with: 38 | bundler-cache: true # 'bundle install' and cache gems 39 | ruby-version: ${{ matrix.ruby }} 40 | bundler: 2.3.26 41 | - name: Run tests 42 | run: bundle exec rspec 43 | 44 | test-prism: 45 | runs-on: ubuntu-latest 46 | env: 47 | BUNDLE_GEMFILE: "gemfiles/rubocop_1.72.gemfile" 48 | PARSER_ENGINE: parser_prism 49 | steps: 50 | - uses: actions/checkout@v4 51 | - name: Set up Ruby 52 | uses: ruby/setup-ruby@v1 53 | with: 54 | bundler-cache: true # 'bundle install' and cache gems 55 | ruby-version: 3.3 56 | bundler: 2.3.26 57 | - name: Run tests 58 | run: bundle exec rspec 59 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | lint-ruby: 15 | name: Ruby 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ruby # Latest stable CRuby version 22 | bundler-cache: true 23 | - name: internal_investigation 24 | run: bundle exec rake internal_investigation 25 | 26 | lint-yaml: 27 | permissions: 28 | contents: read # for actions/checkout to fetch code 29 | name: Yaml 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Yamllint 34 | uses: karancode/yamllint-github-action@v3.0.0 35 | with: 36 | yamllint_strict: true 37 | yamllint_format: parsable 38 | yamllint_comment: true 39 | env: 40 | GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/spell_checking.yml: -------------------------------------------------------------------------------- 1 | name: Spell Checking 2 | 3 | on: [pull_request] 4 | 5 | permissions: # added using https://github.com/step-security/secure-workflows 6 | contents: read 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | codespell: 14 | name: Check spelling of all files with codespell 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: codespell-project/actions-codespell@v2 19 | with: 20 | check_filenames: true 21 | check_hidden: true 22 | misspell: 23 | name: Check spelling of all files in commit with misspell 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Install 28 | run: wget -O - -q https://raw.githubusercontent.com/client9/misspell/master/install-misspell.sh | sh -s -- -b . 29 | - name: Misspell 30 | run: git ls-files --empty-directory | xargs ./misspell -error 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /gemfiles/*.lock 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --warnings 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-internal_affairs 3 | - rubocop-rake 4 | - rubocop-rspec 5 | 6 | AllCops: 7 | DisplayCopNames: true 8 | TargetRubyVersion: 2.7 9 | NewCops: enable 10 | 11 | Lint/RaiseException: 12 | Enabled: true 13 | 14 | Lint/StructNewOverride: 15 | Enabled: true 16 | 17 | Metrics/BlockLength: 18 | Exclude: 19 | - "spec/**/*" 20 | 21 | Metrics/ClassLength: 22 | Enabled: false 23 | 24 | Metrics/MethodLength: 25 | Max: 14 26 | 27 | Naming/FileName: 28 | Exclude: 29 | - lib/rubocop-thread_safety.rb 30 | - rubocop-thread_safety.gemspec 31 | - pkg/**/* 32 | 33 | # Enable more cops that are disabled by default: 34 | 35 | Style/AutoResourceCleanup: 36 | Enabled: true 37 | 38 | Style/CollectionMethods: 39 | Enabled: true 40 | 41 | Style/FormatStringToken: 42 | Exclude: 43 | - spec/**/* 44 | 45 | Style/HashEachMethods: 46 | Enabled: true 47 | 48 | Style/HashTransformKeys: 49 | Enabled: false 50 | 51 | Style/HashTransformValues: 52 | Enabled: false 53 | 54 | Style/MethodCalledOnDoEndBlock: 55 | Enabled: true 56 | Exclude: 57 | - spec/**/* 58 | 59 | Style/MissingElse: 60 | Enabled: true 61 | EnforcedStyle: case 62 | 63 | Style/OptionHash: 64 | Enabled: true 65 | 66 | Style/Send: 67 | Enabled: true 68 | 69 | Style/StringMethods: 70 | Enabled: true 71 | 72 | Style/SymbolArray: 73 | Enabled: true 74 | 75 | RSpec/ExampleLength: 76 | Max: 11 77 | Exclude: 78 | - spec/rubocop/cop/thread_safety/rack_middleware_instance_variable_spec.rb 79 | 80 | RSpec/ContextWording: 81 | Exclude: 82 | - spec/shared_contexts.rb 83 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | extends: default 2 | 3 | rules: 4 | comments: 5 | min-spaces-from-content: 1 6 | document-start: disable 7 | empty-lines: 8 | max: 1 9 | line-length: disable 10 | truthy: 11 | check-keys: false 12 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | customize_gemfiles do 4 | { 5 | single_quotes: true, 6 | heading: <<~HEADING.strip 7 | frozen_string_literal: true 8 | 9 | This file was generated by Appraisal 10 | HEADING 11 | } 12 | end 13 | 14 | appraise 'rubocop-1.72' do 15 | gem 'rubocop', '~> 1.72.1' 16 | end 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## master 4 | 5 | ## 0.7.2 6 | 7 | - [#88](https://github.com/rubocop/rubocop-thread_safety/pull/88): Fix incorrect plugin metadata version. ([@viralpraxis](https://github.com/viralpraxis)) 8 | 9 | ## 0.7.1 10 | 11 | - [#84](https://github.com/rubocop/rubocop-thread_safety/pull/84): Rename `InstanceVariableInClassMethod` in default config ([@sambostock](https://github.com/sambostock)) 12 | 13 | ## 0.7.0 14 | 15 | - [#80](https://github.com/rubocop/rubocop-thread_safety/pull/80) Make RuboCop ThreadSafety work as a RuboCop plugin. ([@bquorning](https://github.com/bquorning)) 16 | - [#76](https://github.com/rubocop/rubocop-thread_safety/pull/76): Detect offenses when using safe navigation for `ThreadSafety/DirChdir`, `ThreadSafety/NewThread` and `ThreadSafety/RackMiddlewareInstanceVariable` cops. ([@viralpraxis](https://github.com/viralpraxis)) 17 | - [#73](https://github.com/rubocop/rubocop-thread_safety/pull/73): Add `AllowCallWithBlock` option to `ThreadSafety/DirChdir` cop. ([@viralpraxis](https://github.com/viralpraxis)) 18 | 19 | ## 0.6.0 20 | 21 | * [#59](https://github.com/rubocop/rubocop-thread_safety/pull/59): Rename `ThreadSafety::InstanceVariableInClassMethod` cop to `ThreadSafety::ClassInstanceVariable` to better reflect its purpose. ([@viralpraxis](https://github.com/viralpraxis)) 22 | * [#55](https://github.com/rubocop/rubocop-thread_safety/pull/55): Enhance `ThreadSafety::InstanceVariableInClassMethod` cop to detect offenses within `class_eval/exec` blocks. ([@viralpraxis](https://github.com/viralpraxis)) 23 | * [#54](https://github.com/rubocop/rubocop-thread_safety/pull/54): Drop support for RuboCop older than 1.48. ([@viralpraxis](https://github.com/viralpraxis)) 24 | * [#52](https://github.com/rubocop/rubocop-thread_safety/pull/52): Add new `RackMiddlewareInstanceVariable` cop to detect instance variables in Rack middleware. ([@viralpraxis](https://github.com/viralpraxis)) 25 | * [#48](https://github.com/rubocop/rubocop-thread_safety/pull/48): Do not report instance variables in `ActionDispatch` callbacks in singleton methods. ([@viralpraxis](https://github.com/viralpraxis)) 26 | * [#43](https://github.com/rubocop/rubocop-thread_safety/pull/43): Make detection of ActiveSupport's `class_attribute` configurable. ([@viralpraxis](https://github.com/viralpraxis)) 27 | * [#42](https://github.com/rubocop/rubocop-thread_safety/pull/42): Fix some `InstanceVariableInClassMethod` cop false positive offenses. ([@viralpraxis](https://github.com/viralpraxis)) 28 | * [#41](https://github.com/rubocop/rubocop-thread_safety/pull/41): Drop support for MRI older than 2.7. ([@viralpraxis](https://github.com/viralpraxis)) 29 | * [#38](https://github.com/rubocop/rubocop-thread_safety/pull/38): Fix `NewThread` cop detection is case of `Thread.start`, `Thread.fork`, or `Thread.new` with arguments. ([@viralpraxis](https://github.com/viralpraxis)) 30 | * [#36](https://github.com/rubocop/rubocop-thread_safety/pull/36): Add new `DirChdir` cop to detect `Dir.chdir` calls. ([@viralpraxis](https://github.com/viralpraxis)) 31 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | gem 'appraisal' 8 | gem 'bundler', '>= 1.10', '< 3' 9 | gem 'prism', '~> 1.2.0' 10 | gem 'pry' unless ENV['CI'] 11 | gem 'rake', '>= 10.0' 12 | gem 'rspec', '~> 3.0' 13 | gem 'rubocop', github: 'rubocop/rubocop' 14 | gem 'rubocop-rake', '~> 0.7' 15 | gem 'rubocop-rspec', '~> 3.5' 16 | gem 'simplecov' 17 | gem 'yard' 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Portions Copyright 2016-2023 Michael Gee and contributors 2 | Portions Copyright 2016-2022 CoverMyMeds 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 | # RuboCop::ThreadSafety 2 | 3 | Thread-safety analysis for your projects, as an extension to 4 | [RuboCop](https://github.com/rubocop/rubocop). 5 | 6 | ## Installation and Usage 7 | 8 | ### Installation into an application 9 | 10 | Add this line to your application's Gemfile (using `require: false` as it's a standalone tool): 11 | 12 | ```ruby 13 | gem 'rubocop-thread_safety', require: false 14 | ``` 15 | 16 | Install it with Bundler by invoking: 17 | 18 | $ bundle 19 | 20 | Add this line to your application's `.rubocop.yml`: 21 | 22 | plugins: rubocop-thread_safety 23 | 24 | Now you can run `rubocop` and it will automatically load the RuboCop 25 | Thread-Safety cops together with the standard cops. 26 | 27 | > [!NOTE] 28 | > The plugin system is supported in RuboCop 1.72+. In earlier versions, use `require` instead of `plugins`. 29 | 30 | ### Scanning an application without adding it to the Gemfile 31 | 32 | Install the gem: 33 | 34 | $ gem install rubocop-thread_safety 35 | 36 | Scan the application for just thread-safety issues: 37 | 38 | $ rubocop --plugin rubocop-thread_safety --only ThreadSafety,Style/GlobalVars,Style/ClassVars,Style/MutableConstant 39 | 40 | ### Configuration 41 | 42 | There are some added [configuration options](https://github.com/rubocop/rubocop-thread_safety/blob/master/config/default.yml) that can be tweaked to modify the behaviour of these thread-safety cops. 43 | 44 | ### Correcting code for thread-safety 45 | 46 | There are a few ways to improve thread-safety that stem around avoiding 47 | unsynchronized mutation of state that is shared between multiple threads. 48 | 49 | State shared between threads may take various forms, including: 50 | 51 | * Class variables (`@@name`). Note: these affect child classes too. 52 | * Class instance variables (`@name` in class context or class methods) 53 | * Constants (`NAME`). Ruby will warn if a constant is re-assigned to a new value but will allow it. Mutable objects can still be mutated (e.g. push to an array) even if they are assigned to a constant. 54 | * Globals (`$name`), with the possible exception of some special globals provided by ruby that are documented as thread-local like regular expression results. 55 | * Variables in the scope of created threads (where `Thread.new` is called). 56 | 57 | Improvements that would make shared state thread-safe include: 58 | 59 | * `freeze` objects to protect against mutation. Note: `freeze` is shallow, i.e. freezing an array will not also freeze its elements. 60 | * Use data structures or concurrency abstractions from [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby), e.g. `Concurrent::Map` 61 | * Use a `Mutex` or similar to `synchronize` access. 62 | * Use [`ActiveSupport::CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) 63 | * Use [`RequestStore`](https://github.com/steveklabnik/request_store) 64 | * Use `Thread.current[:name]` 65 | 66 | Certain system calls, such as `chdir`, affect the entire process. To avoid potential thread-safety issues, it's preferable to use (if possible) the `chdir` option in methods like `Kernel.system` and `IO.popen` rather than relying on `Dir.chdir`. 67 | 68 | ## Development 69 | 70 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 71 | 72 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 73 | 74 | ## Contributing 75 | 76 | Bug reports and pull requests are welcome on GitHub at https://github.com/rubocop/rubocop-thread_safety. 77 | 78 | ## Copyright 79 | 80 | Portions Copyright (c) 2016-2023 Michael Gee and [contributors](https://github.com/rubocop/rubocop-thread_safety/graphs/contributors). 81 | Portions Copyright (c) 2016-2023 CoverMyMeds. 82 | 83 | See [LICENSE.txt](LICENSE.txt) for further details. 84 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'open3' 4 | 5 | require 'bundler' 6 | require 'bundler/gem_tasks' 7 | 8 | begin 9 | Bundler.setup(:default, :development) 10 | rescue Bundler::BundlerError => e 11 | warn e.message 12 | warn 'Run `bundle install` to install missing gems' 13 | exit e.status_code 14 | end 15 | 16 | require 'rspec/core/rake_task' 17 | require 'rubocop/rake_task' 18 | 19 | Dir['tasks/**/*.rake'].each { |t| load t } 20 | 21 | RSpec::Core::RakeTask.new(:spec) do |spec| 22 | spec.pattern = FileList['spec/**/*_spec.rb'] 23 | end 24 | 25 | desc 'Run RuboCop over this gem' 26 | RuboCop::RakeTask.new(:internal_investigation) 27 | 28 | desc 'Confirm documentation is up to date' 29 | task confirm_documentation: :generate_cops_documentation do 30 | _, _, _, process = 31 | Open3.popen3('git diff --exit-code docs/') 32 | 33 | unless process.value.success? 34 | raise 'Please run `rake generate_cops_documentation` ' \ 35 | 'and add docs/ to the commit.' 36 | end 37 | end 38 | 39 | task default: %i[spec 40 | internal_investigation 41 | documentation_syntax_check 42 | confirm_documentation] 43 | 44 | desc 'Generate a new cop template' 45 | task :new_cop, [:cop] do |_task, args| 46 | require 'rubocop' 47 | 48 | cop_name = args.fetch(:cop) do 49 | warn "usage: bundle exec rake 'new_cop[ThreadSafety/Name]'" 50 | exit! 51 | end 52 | 53 | generator = RuboCop::Cop::Generator.new(cop_name) 54 | 55 | generator.write_source 56 | generator.write_spec 57 | generator.inject_require(root_file_path: 'lib/rubocop-thread_safety.rb') 58 | generator.inject_config(config_file_path: 'config/default.yml') 59 | 60 | puts generator.todo 61 | end 62 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # frozen_string_literal: true 4 | 5 | require 'bundler/setup' 6 | require 'rubocop-thread_safety' 7 | 8 | # You can add fixtures and/or initialization code here to make experimenting 9 | # with your gem easier. You can also use a different console, if you like. 10 | 11 | require 'pry' 12 | Pry.start 13 | 14 | # require "irb" 15 | # IRB.start 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /config/default.yml: -------------------------------------------------------------------------------- 1 | # Additional configuration for thread_safety cops 2 | # 3 | # Without adding these to your rubocop config, these values will be the default. 4 | 5 | ThreadSafety/ClassAndModuleAttributes: 6 | Description: 'Avoid mutating class and module attributes.' 7 | Enabled: true 8 | ActiveSupportClassAttributeAllowed: false 9 | 10 | ThreadSafety/ClassInstanceVariable: 11 | Description: 'Avoid class instance variables.' 12 | Enabled: true 13 | 14 | ThreadSafety/MutableClassInstanceVariable: 15 | Description: 'Do not assign mutable objects to class instance variables.' 16 | Enabled: true 17 | EnforcedStyle: literals 18 | SafeAutoCorrect: false 19 | SupportedStyles: 20 | # literals: freeze literals assigned to constants 21 | # strict: freeze all constants 22 | # Strict mode is considered an experimental feature. It has not been updated 23 | # with an exhaustive list of all methods that will produce frozen objects so 24 | # there is a decent chance of getting some false positives. Luckily, there is 25 | # no harm in freezing an already frozen object. 26 | - literals 27 | - strict 28 | 29 | ThreadSafety/NewThread: 30 | Description: >- 31 | Avoid starting new threads. 32 | Let a framework like Sidekiq handle the threads. 33 | Enabled: true 34 | 35 | ThreadSafety/DirChdir: 36 | Description: Avoid using `Dir.chdir` due to its process-wide effect. 37 | Enabled: true 38 | AllowCallWithBlock: false 39 | 40 | ThreadSafety/RackMiddlewareInstanceVariable: 41 | Description: Avoid instance variables in Rack middleware. 42 | Enabled: true 43 | Include: 44 | - 'app/middleware/**/*.rb' 45 | - 'lib/middleware/**/*.rb' 46 | - 'app/middlewares/**/*.rb' 47 | - 'lib/middlewares/**/*.rb' 48 | AllowedIdentifiers: [] 49 | -------------------------------------------------------------------------------- /config/obsoletion.yml: -------------------------------------------------------------------------------- 1 | renamed: 2 | ThreadSafety/InstanceVariableInClassMethod: ThreadSafety/ClassInstanceVariable 3 | -------------------------------------------------------------------------------- /docs/antora.yml: -------------------------------------------------------------------------------- 1 | name: rubocop-thread_safety 2 | title: RuboCop Thread Safety 3 | version: ~ 4 | nav: 5 | - modules/ROOT/nav.adoc 6 | -------------------------------------------------------------------------------- /docs/modules/ROOT/nav.adoc: -------------------------------------------------------------------------------- 1 | * xref:index.adoc[Home] 2 | * xref:installation.adoc[Installation] 3 | * xref:usage.adoc[Usage] 4 | * xref:cops.adoc[Cops] 5 | * Cops Documentation 6 | ** xref:cops_threadsafety.adoc[Thread Safety] 7 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/cops.adoc: -------------------------------------------------------------------------------- 1 | === Department xref:cops_threadsafety.adoc[ThreadSafety] 2 | 3 | * xref:cops_threadsafety.adoc#threadsafetyclassandmoduleattributes[ThreadSafety/ClassAndModuleAttributes] 4 | * xref:cops_threadsafety.adoc#threadsafetyclassinstancevariable[ThreadSafety/ClassInstanceVariable] 5 | * xref:cops_threadsafety.adoc#threadsafetymutableclassinstancevariable[ThreadSafety/MutableClassInstanceVariable] 6 | * xref:cops_threadsafety.adoc#threadsafetynewthread[ThreadSafety/NewThread] 7 | * xref:cops_threadsafety.adoc#threadsafetydirchdir[ThreadSafety/DirChdir] 8 | * xref:cops_threadsafety.adoc#threadsafetyrackmiddlewareinstancevariable[ThreadSafety/RackMiddlewareInstanceVariable] 9 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/cops_threadsafety.adoc: -------------------------------------------------------------------------------- 1 | //// 2 | Do NOT edit this file by hand directly, as it is automatically generated. 3 | 4 | Please make any necessary changes to the cop documentation within the source files themselves. 5 | //// 6 | 7 | = ThreadSafety 8 | 9 | [#threadsafetyclassandmoduleattributes] 10 | == ThreadSafety/ClassAndModuleAttributes 11 | 12 | |=== 13 | | Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed 14 | 15 | | Enabled 16 | | Yes 17 | | No 18 | | - 19 | | - 20 | |=== 21 | 22 | Avoid mutating class and module attributes. 23 | 24 | They are implemented by class variables, which are not thread-safe. 25 | 26 | [#examples-threadsafetyclassandmoduleattributes] 27 | === Examples 28 | 29 | [source,ruby] 30 | ---- 31 | # bad 32 | class User 33 | cattr_accessor :current_user 34 | end 35 | ---- 36 | 37 | [#configurable-attributes-threadsafetyclassandmoduleattributes] 38 | === Configurable attributes 39 | 40 | |=== 41 | | Name | Default value | Configurable values 42 | 43 | | ActiveSupportClassAttributeAllowed 44 | | `false` 45 | | Boolean 46 | |=== 47 | 48 | [#threadsafetyclassinstancevariable] 49 | == ThreadSafety/ClassInstanceVariable 50 | 51 | |=== 52 | | Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed 53 | 54 | | Enabled 55 | | Yes 56 | | No 57 | | - 58 | | - 59 | |=== 60 | 61 | Avoid class instance variables. 62 | 63 | [#examples-threadsafetyclassinstancevariable] 64 | === Examples 65 | 66 | [source,ruby] 67 | ---- 68 | # bad 69 | class User 70 | def self.notify(info) 71 | @info = validate(info) 72 | Notifier.new(@info).deliver 73 | end 74 | end 75 | 76 | class Model 77 | class << self 78 | def table_name(name) 79 | @table_name = name 80 | end 81 | end 82 | end 83 | 84 | class Host 85 | %i[uri port].each do |key| 86 | define_singleton_method("#{key}=") do |value| 87 | instance_variable_set("@#{key}", value) 88 | end 89 | end 90 | end 91 | 92 | module Example 93 | module ClassMethods 94 | def test(params) 95 | @params = params 96 | end 97 | end 98 | end 99 | 100 | module Example 101 | class_methods do 102 | def test(params) 103 | @params = params 104 | end 105 | end 106 | end 107 | 108 | module Example 109 | module_function 110 | 111 | def test(params) 112 | @params = params 113 | end 114 | end 115 | 116 | module Example 117 | def test(params) 118 | @params = params 119 | end 120 | 121 | module_function :test 122 | end 123 | ---- 124 | 125 | [#threadsafetydirchdir] 126 | == ThreadSafety/DirChdir 127 | 128 | |=== 129 | | Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed 130 | 131 | | Enabled 132 | | Yes 133 | | No 134 | | - 135 | | - 136 | |=== 137 | 138 | Avoid using `Dir.chdir` due to its process-wide effect. 139 | If `AllowCallWithBlock` (disabled by default) option is enabled, 140 | calling `Dir.chdir` with block will be allowed. 141 | 142 | [#examples-threadsafetydirchdir] 143 | === Examples 144 | 145 | [source,ruby] 146 | ---- 147 | # bad 148 | Dir.chdir("/var/run") 149 | 150 | # bad 151 | FileUtils.chdir("/var/run") 152 | ---- 153 | 154 | [#allowcallwithblock_-false-_default_-threadsafetydirchdir] 155 | ==== AllowCallWithBlock: false (default) 156 | 157 | [source,ruby] 158 | ---- 159 | # good 160 | Dir.chdir("/var/run") do 161 | puts Dir.pwd 162 | end 163 | ---- 164 | 165 | [#allowcallwithblock_-true-threadsafetydirchdir] 166 | ==== AllowCallWithBlock: true 167 | 168 | [source,ruby] 169 | ---- 170 | # bad 171 | Dir.chdir("/var/run") do 172 | puts Dir.pwd 173 | end 174 | ---- 175 | 176 | [#configurable-attributes-threadsafetydirchdir] 177 | === Configurable attributes 178 | 179 | |=== 180 | | Name | Default value | Configurable values 181 | 182 | | AllowCallWithBlock 183 | | `false` 184 | | Boolean 185 | |=== 186 | 187 | [#threadsafetymutableclassinstancevariable] 188 | == ThreadSafety/MutableClassInstanceVariable 189 | 190 | |=== 191 | | Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed 192 | 193 | | Enabled 194 | | Yes 195 | | Always (Unsafe) 196 | | - 197 | | - 198 | |=== 199 | 200 | Checks whether some class instance variable isn't a 201 | mutable literal (e.g. array or hash). 202 | 203 | It is based on Style/MutableConstant from RuboCop. 204 | See https://github.com/rubocop/rubocop/blob/master/lib/rubocop/cop/style/mutable_constant.rb 205 | 206 | Class instance variables are a risk to threaded code as they are shared 207 | between threads. A mutable object such as an array or hash may be 208 | updated via an attr_reader so would not be detected by the 209 | ThreadSafety/ClassAndModuleAttributes cop. 210 | 211 | Strict mode can be used to freeze all class instance variables, rather 212 | than just literals. 213 | Strict mode is considered an experimental feature. It has not been 214 | updated with an exhaustive list of all methods that will produce frozen 215 | objects so there is a decent chance of getting some false positives. 216 | Luckily, there is no harm in freezing an already frozen object. 217 | 218 | [#examples-threadsafetymutableclassinstancevariable] 219 | === Examples 220 | 221 | [#enforcedstyle_-literals-_default_-threadsafetymutableclassinstancevariable] 222 | ==== EnforcedStyle: literals (default) 223 | 224 | [source,ruby] 225 | ---- 226 | # bad 227 | class Model 228 | @list = [1, 2, 3] 229 | end 230 | 231 | # good 232 | class Model 233 | @list = [1, 2, 3].freeze 234 | end 235 | 236 | # good 237 | class Model 238 | @var = <<~TESTING.freeze 239 | This is a heredoc 240 | TESTING 241 | end 242 | 243 | # good 244 | class Model 245 | @var = Something.new 246 | end 247 | ---- 248 | 249 | [#enforcedstyle_-strict-threadsafetymutableclassinstancevariable] 250 | ==== EnforcedStyle: strict 251 | 252 | [source,ruby] 253 | ---- 254 | # bad 255 | class Model 256 | @var = Something.new 257 | end 258 | 259 | # bad 260 | class Model 261 | @var = Struct.new do 262 | def foo 263 | puts 1 264 | end 265 | end 266 | end 267 | 268 | # good 269 | class Model 270 | @var = Something.new.freeze 271 | end 272 | 273 | # good 274 | class Model 275 | @var = Struct.new do 276 | def foo 277 | puts 1 278 | end 279 | end.freeze 280 | end 281 | ---- 282 | 283 | [#configurable-attributes-threadsafetymutableclassinstancevariable] 284 | === Configurable attributes 285 | 286 | |=== 287 | | Name | Default value | Configurable values 288 | 289 | | EnforcedStyle 290 | | `literals` 291 | | `literals`, `strict` 292 | |=== 293 | 294 | [#threadsafetynewthread] 295 | == ThreadSafety/NewThread 296 | 297 | |=== 298 | | Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed 299 | 300 | | Enabled 301 | | Yes 302 | | No 303 | | - 304 | | - 305 | |=== 306 | 307 | Avoid starting new threads. 308 | 309 | Let a framework like Sidekiq handle the threads. 310 | 311 | [#examples-threadsafetynewthread] 312 | === Examples 313 | 314 | [source,ruby] 315 | ---- 316 | # bad 317 | Thread.new { do_work } 318 | ---- 319 | 320 | [#threadsafetyrackmiddlewareinstancevariable] 321 | == ThreadSafety/RackMiddlewareInstanceVariable 322 | 323 | |=== 324 | | Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed 325 | 326 | | Enabled 327 | | Yes 328 | | No 329 | | - 330 | | - 331 | |=== 332 | 333 | Avoid instance variables in rack middleware. 334 | 335 | Middlewares are initialized once, meaning any instance variables are shared between executor threads. 336 | To avoid potential race conditions, it's recommended to design middlewares to be stateless 337 | or to implement proper synchronization mechanisms. 338 | 339 | [#examples-threadsafetyrackmiddlewareinstancevariable] 340 | === Examples 341 | 342 | [source,ruby] 343 | ---- 344 | # bad 345 | class CounterMiddleware 346 | def initialize(app) 347 | @app = app 348 | @counter = 0 349 | end 350 | 351 | def call(env) 352 | app.call(env) 353 | ensure 354 | @counter += 1 355 | end 356 | end 357 | 358 | # good 359 | class CounterMiddleware 360 | def initialize(app) 361 | @app = app 362 | @counter = Concurrent::AtomicReference.new(0) 363 | end 364 | 365 | def call(env) 366 | app.call(env) 367 | ensure 368 | @counter.update { |ref| ref + 1 } 369 | end 370 | end 371 | 372 | class IdentityMiddleware 373 | def initialize(app) 374 | @app = app 375 | end 376 | 377 | def call(env) 378 | app.call(env) 379 | end 380 | end 381 | ---- 382 | 383 | [#configurable-attributes-threadsafetyrackmiddlewareinstancevariable] 384 | === Configurable attributes 385 | 386 | |=== 387 | | Name | Default value | Configurable values 388 | 389 | | Include 390 | | `+app/middleware/**/*.rb+`, `+lib/middleware/**/*.rb+`, `+app/middlewares/**/*.rb+`, `+lib/middlewares/**/*.rb+` 391 | | Array 392 | 393 | | AllowedIdentifiers 394 | | `[]` 395 | | Array 396 | |=== 397 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/index.adoc: -------------------------------------------------------------------------------- 1 | = RuboCop Thread Safety 2 | 3 | Thread safety analysis for your projects, as an extension to 4 | https://github.com/rubocop/rubocop[RuboCop]. 5 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/installation.adoc: -------------------------------------------------------------------------------- 1 | = Installation 2 | 3 | Just install the `rubocop-thread_safety` gem 4 | 5 | [source,bash] 6 | ---- 7 | gem install rubocop-thread_safety 8 | ---- 9 | 10 | or if you use bundler put this in your `Gemfile` 11 | 12 | [source,ruby] 13 | ---- 14 | gem 'rubocop-thread_safety' 15 | ---- 16 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/usage.adoc: -------------------------------------------------------------------------------- 1 | = Usage 2 | 3 | You need to tell RuboCop to load the Thread Safety extension. There are three 4 | ways to do this: 5 | 6 | == RuboCop configuration file 7 | 8 | Put this into your `.rubocop.yml`. 9 | 10 | [source,yaml] 11 | ---- 12 | plugins: rubocop-thread_safety 13 | ---- 14 | 15 | Now you can run `rubocop` and it will automatically load the RuboCop Thread Safety 16 | cops together with the standard cops. 17 | 18 | == Command line 19 | 20 | [source,sh] 21 | ---- 22 | $ rubocop --plugin rubocop-thread_safety 23 | ---- 24 | 25 | == Rake task 26 | 27 | [source,ruby] 28 | ---- 29 | RuboCop::RakeTask.new do |task| 30 | task.requires << 'rubocop-thread_safety' 31 | end 32 | ---- 33 | -------------------------------------------------------------------------------- /gemfiles/rubocop_1.72.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by Appraisal 4 | 5 | source 'https://rubygems.org' 6 | 7 | gem 'appraisal' 8 | gem 'bundler', '>= 1.10', '< 3' 9 | gem 'prism', '~> 1.2.0' 10 | gem 'pry' 11 | gem 'rake', '>= 10.0' 12 | gem 'rspec', '~> 3.0' 13 | gem 'rubocop', '~> 1.72.1' 14 | gem 'rubocop-rake', '~> 0.7' 15 | gem 'rubocop-rspec', '~> 3.5' 16 | gem 'simplecov' 17 | gem 'yard' 18 | 19 | gemspec path: '../' 20 | -------------------------------------------------------------------------------- /lib/rubocop-thread_safety.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubocop' 4 | 5 | require 'rubocop/thread_safety' 6 | require 'rubocop/thread_safety/version' 7 | require 'rubocop/thread_safety/plugin' 8 | 9 | require 'rubocop/cop/mixin/operation_with_threadsafe_result' 10 | 11 | require 'rubocop/cop/thread_safety/class_instance_variable' 12 | require 'rubocop/cop/thread_safety/class_and_module_attributes' 13 | require 'rubocop/cop/thread_safety/mutable_class_instance_variable' 14 | require 'rubocop/cop/thread_safety/new_thread' 15 | require 'rubocop/cop/thread_safety/dir_chdir' 16 | require 'rubocop/cop/thread_safety/rack_middleware_instance_variable' 17 | -------------------------------------------------------------------------------- /lib/rubocop/cop/mixin/operation_with_threadsafe_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | module Cop 5 | # Common functionality for checking if a well-known operation 6 | # produces an object with thread-safe semantics. 7 | module OperationWithThreadsafeResult 8 | extend NodePattern::Macros 9 | 10 | # @!method operation_produces_threadsafe_object?(node) 11 | def_node_matcher :operation_produces_threadsafe_object?, <<~PATTERN 12 | { 13 | (send (const {nil? cbase} :Queue) :new ...) 14 | (send 15 | (const (const {nil? cbase} :ThreadSafe) {:Hash :Array}) 16 | :new ...) 17 | (block 18 | (send 19 | (const (const {nil? cbase} :ThreadSafe) {:Hash :Array}) 20 | :new ...) 21 | ...) 22 | (send (const (const {nil? cbase} :Concurrent) _) :new ...) 23 | (block 24 | (send (const (const {nil? cbase} :Concurrent) _) :new ...) 25 | ...) 26 | (send (const (const (const {nil? cbase} :Concurrent) _) _) :new ...) 27 | (block 28 | (send 29 | (const (const (const {nil? cbase} :Concurrent) _) _) 30 | :new ...) 31 | ...) 32 | (send 33 | (const (const (const (const {nil? cbase} :Concurrent) _) _) _) 34 | :new ...) 35 | (block 36 | (send 37 | (const (const (const (const {nil? cbase} :Concurrent) _) _) _) 38 | :new ...) 39 | ...) 40 | } 41 | PATTERN 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/rubocop/cop/thread_safety/class_and_module_attributes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | module Cop 5 | module ThreadSafety 6 | # Avoid mutating class and module attributes. 7 | # 8 | # They are implemented by class variables, which are not thread-safe. 9 | # 10 | # @example 11 | # # bad 12 | # class User 13 | # cattr_accessor :current_user 14 | # end 15 | class ClassAndModuleAttributes < Base 16 | MSG = 'Avoid mutating class and module attributes.' 17 | RESTRICT_ON_SEND = %i[ 18 | mattr_writer mattr_accessor cattr_writer cattr_accessor 19 | class_attribute 20 | attr attr_accessor attr_writer 21 | attr_internal attr_internal_accessor attr_internal_writer 22 | ].freeze 23 | 24 | # @!method mattr?(node) 25 | def_node_matcher :mattr?, <<~MATCHER 26 | (send nil? 27 | {:mattr_writer :mattr_accessor :cattr_writer :cattr_accessor} 28 | ...) 29 | MATCHER 30 | 31 | # @!method attr?(node) 32 | def_node_matcher :attr?, <<~MATCHER 33 | (send nil? 34 | {:attr :attr_accessor :attr_writer} 35 | ...) 36 | MATCHER 37 | 38 | # @!method attr_internal?(node) 39 | def_node_matcher :attr_internal?, <<~MATCHER 40 | (send nil? 41 | {:attr_internal :attr_internal_accessor :attr_internal_writer} 42 | ...) 43 | MATCHER 44 | 45 | # @!method class_attr?(node) 46 | def_node_matcher :class_attr?, <<~MATCHER 47 | (send nil? 48 | :class_attribute 49 | ...) 50 | MATCHER 51 | 52 | def on_send(node) # rubocop:disable InternalAffairs/OnSendWithoutOnCSend 53 | return unless mattr?(node) || (!class_attribute_allowed? && class_attr?(node)) || singleton_attr?(node) 54 | 55 | add_offense(node) 56 | end 57 | 58 | private 59 | 60 | def singleton_attr?(node) 61 | (attr?(node) || attr_internal?(node)) && 62 | defined_in_singleton_class?(node) 63 | end 64 | 65 | def defined_in_singleton_class?(node) 66 | node.ancestors.each do |ancestor| 67 | case ancestor.type 68 | when :def then return false 69 | when :sclass then return true 70 | else next 71 | end 72 | end 73 | 74 | false 75 | end 76 | 77 | def class_attribute_allowed? 78 | cop_config['ActiveSupportClassAttributeAllowed'] 79 | end 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/rubocop/cop/thread_safety/class_instance_variable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | module Cop 5 | module ThreadSafety 6 | # Avoid class instance variables. 7 | # 8 | # @example 9 | # # bad 10 | # class User 11 | # def self.notify(info) 12 | # @info = validate(info) 13 | # Notifier.new(@info).deliver 14 | # end 15 | # end 16 | # 17 | # class Model 18 | # class << self 19 | # def table_name(name) 20 | # @table_name = name 21 | # end 22 | # end 23 | # end 24 | # 25 | # class Host 26 | # %i[uri port].each do |key| 27 | # define_singleton_method("#{key}=") do |value| 28 | # instance_variable_set("@#{key}", value) 29 | # end 30 | # end 31 | # end 32 | # 33 | # module Example 34 | # module ClassMethods 35 | # def test(params) 36 | # @params = params 37 | # end 38 | # end 39 | # end 40 | # 41 | # module Example 42 | # class_methods do 43 | # def test(params) 44 | # @params = params 45 | # end 46 | # end 47 | # end 48 | # 49 | # module Example 50 | # module_function 51 | # 52 | # def test(params) 53 | # @params = params 54 | # end 55 | # end 56 | # 57 | # module Example 58 | # def test(params) 59 | # @params = params 60 | # end 61 | # 62 | # module_function :test 63 | # end 64 | class ClassInstanceVariable < Base 65 | MSG = 'Avoid class instance variables.' 66 | RESTRICT_ON_SEND = %i[ 67 | instance_variable_set 68 | instance_variable_get 69 | ].freeze 70 | 71 | # @!method instance_variable_set_call?(node) 72 | def_node_matcher :instance_variable_set_call?, <<~MATCHER 73 | (send nil? :instance_variable_set (...) (...)) 74 | MATCHER 75 | 76 | # @!method instance_variable_get_call?(node) 77 | def_node_matcher :instance_variable_get_call?, <<~MATCHER 78 | (send nil? :instance_variable_get (...)) 79 | MATCHER 80 | 81 | def on_ivar(node) 82 | return unless class_method_definition?(node) 83 | return if method_definition?(node) 84 | return if synchronized?(node) 85 | 86 | add_offense(node.loc.name) 87 | end 88 | alias on_ivasgn on_ivar 89 | 90 | def on_send(node) # rubocop:disable InternalAffairs/OnSendWithoutOnCSend 91 | return unless instance_variable_call?(node) 92 | return unless class_method_definition?(node) 93 | return if method_definition?(node) 94 | return if synchronized?(node) 95 | 96 | add_offense(node) 97 | end 98 | 99 | private 100 | 101 | def class_method_definition?(node) 102 | in_defs?(node) || 103 | in_def_sclass?(node) || 104 | in_def_class_methods?(node) || 105 | in_def_module_function?(node) || 106 | in_class_eval?(node) || 107 | singleton_method_definition?(node) 108 | end 109 | 110 | def in_defs?(node) 111 | node.ancestors.any? do |ancestor| 112 | break false if new_lexical_scope?(ancestor) 113 | 114 | ancestor.defs_type? 115 | end 116 | end 117 | 118 | def in_def_sclass?(node) 119 | defn = node.ancestors.find do |ancestor| 120 | break if new_lexical_scope?(ancestor) 121 | 122 | ancestor.def_type? 123 | end 124 | 125 | defn&.ancestors&.any?(&:sclass_type?) 126 | end 127 | 128 | def in_def_class_methods?(node) 129 | in_def_class_methods_dsl?(node) || in_def_class_methods_module?(node) 130 | end 131 | 132 | def in_def_class_methods_dsl?(node) 133 | node.ancestors.any? do |ancestor| 134 | break if new_lexical_scope?(ancestor) 135 | next unless ancestor.block_type? 136 | 137 | ancestor.children.first.command? :class_methods 138 | end 139 | end 140 | 141 | def in_def_class_methods_module?(node) 142 | defn = node.ancestors.find do |ancestor| 143 | break if new_lexical_scope?(ancestor) 144 | 145 | ancestor.def_type? 146 | end 147 | return false unless defn 148 | 149 | mod = defn.ancestors.find do |ancestor| 150 | %i[class module].include?(ancestor.type) 151 | end 152 | return false unless mod 153 | 154 | class_methods_module?(mod) 155 | end 156 | 157 | def in_def_module_function?(node) 158 | defn = node.ancestors.find(&:def_type?) 159 | return false unless defn 160 | 161 | defn.left_siblings.any? { |sibling| module_function_bare_access_modifier?(sibling) } || 162 | defn.right_siblings.any? { |sibling| module_function_for?(sibling, defn.method_name) } 163 | end 164 | 165 | def in_class_eval?(node) 166 | defn = node.ancestors.find do |ancestor| 167 | break if ancestor.def_type? || new_lexical_scope?(ancestor) 168 | 169 | ancestor.block_type? 170 | end 171 | return false unless defn 172 | 173 | class_eval_scope?(defn) 174 | end 175 | 176 | def singleton_method_definition?(node) 177 | node.ancestors.any? do |ancestor| 178 | break if new_lexical_scope?(ancestor) 179 | next unless ancestor.children.first.is_a? AST::SendNode 180 | 181 | ancestor.children.first.command? :define_singleton_method 182 | end 183 | end 184 | 185 | def method_definition?(node) 186 | node.ancestors.any? do |ancestor| 187 | break if new_lexical_scope?(ancestor) 188 | next unless ancestor.children.first.is_a? AST::SendNode 189 | 190 | ancestor.children.first.command? :define_method 191 | end 192 | end 193 | 194 | def synchronized?(node) 195 | node.ancestors.find do |ancestor| 196 | next unless ancestor.block_type? 197 | 198 | s = ancestor.children.first 199 | s.send_type? && s.children.last == :synchronize 200 | end 201 | end 202 | 203 | def instance_variable_call?(node) 204 | instance_variable_set_call?(node) || instance_variable_get_call?(node) 205 | end 206 | 207 | def module_function_bare_access_modifier?(node) 208 | return false unless node.respond_to?(:send_type?) 209 | 210 | node.send_type? && node.bare_access_modifier? && node.method?(:module_function) 211 | end 212 | 213 | def match_name?(arg_name, method_name) 214 | arg_name.to_sym == method_name.to_sym 215 | end 216 | 217 | # @!method class_methods_module?(node) 218 | def_node_matcher :class_methods_module?, <<~PATTERN 219 | (module (const _ :ClassMethods) ...) 220 | PATTERN 221 | 222 | # @!method module_function_for?(node) 223 | def_node_matcher :module_function_for?, <<~PATTERN 224 | (send nil? {:module_function} ({sym str} #match_name?(%1))) 225 | PATTERN 226 | 227 | # @!method new_lexical_scope?(node) 228 | def_node_matcher :new_lexical_scope?, <<~PATTERN 229 | { 230 | (block (send (const nil? :Struct) :new ...) _ ({def defs} ...)) 231 | (block (send (const nil? :Class) :new ...) _ ({def defs} ...)) 232 | (block (send (const nil? :Data) :define ...) _ ({def defs} ...)) 233 | (block 234 | (send nil? 235 | { 236 | :prepend_around_action 237 | :prepend_before_action 238 | :before_action 239 | :append_before_action 240 | :around_action 241 | :append_around_action 242 | :append_after_action 243 | :after_action 244 | :prepend_after_action 245 | } 246 | ) 247 | ... 248 | ) 249 | } 250 | PATTERN 251 | 252 | # @!method class_eval_scope?(node) 253 | def_node_matcher :class_eval_scope?, <<~PATTERN 254 | (block (send (const {nil? cbase} _) {:class_eval :class_exec}) ...) 255 | PATTERN 256 | end 257 | end 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /lib/rubocop/cop/thread_safety/dir_chdir.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | module Cop 5 | module ThreadSafety 6 | # Avoid using `Dir.chdir` due to its process-wide effect. 7 | # If `AllowCallWithBlock` (disabled by default) option is enabled, 8 | # calling `Dir.chdir` with block will be allowed. 9 | # 10 | # @example 11 | # # bad 12 | # Dir.chdir("/var/run") 13 | # 14 | # # bad 15 | # FileUtils.chdir("/var/run") 16 | # 17 | # @example AllowCallWithBlock: false (default) 18 | # # good 19 | # Dir.chdir("/var/run") do 20 | # puts Dir.pwd 21 | # end 22 | # 23 | # @example AllowCallWithBlock: true 24 | # # bad 25 | # Dir.chdir("/var/run") do 26 | # puts Dir.pwd 27 | # end 28 | # 29 | class DirChdir < Base 30 | MESSAGE = 'Avoid using `%s%s%s` due to its process-wide effect.' 31 | RESTRICT_ON_SEND = %i[chdir cd].freeze 32 | 33 | # @!method chdir?(node) 34 | def_node_matcher :chdir?, <<~MATCHER 35 | { 36 | (call (const {nil? cbase} {:Dir :FileUtils}) :chdir ...) 37 | (call (const {nil? cbase} :FileUtils) :cd ...) 38 | } 39 | MATCHER 40 | 41 | def on_send(node) 42 | return unless chdir?(node) 43 | return if allow_call_with_block? && (node.block_argument? || node.parent&.block_type?) 44 | 45 | add_offense( 46 | node, 47 | message: format( 48 | MESSAGE, 49 | module: node.receiver.short_name, 50 | method: node.method_name, 51 | dot: node.loc.dot.source 52 | ) 53 | ) 54 | end 55 | alias on_csend on_send 56 | 57 | private 58 | 59 | def allow_call_with_block? 60 | !!cop_config['AllowCallWithBlock'] 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | module Cop 5 | module ThreadSafety 6 | # Checks whether some class instance variable isn't a 7 | # mutable literal (e.g. array or hash). 8 | # 9 | # It is based on Style/MutableConstant from RuboCop. 10 | # See https://github.com/rubocop/rubocop/blob/master/lib/rubocop/cop/style/mutable_constant.rb 11 | # 12 | # Class instance variables are a risk to threaded code as they are shared 13 | # between threads. A mutable object such as an array or hash may be 14 | # updated via an attr_reader so would not be detected by the 15 | # ThreadSafety/ClassAndModuleAttributes cop. 16 | # 17 | # Strict mode can be used to freeze all class instance variables, rather 18 | # than just literals. 19 | # Strict mode is considered an experimental feature. It has not been 20 | # updated with an exhaustive list of all methods that will produce frozen 21 | # objects so there is a decent chance of getting some false positives. 22 | # Luckily, there is no harm in freezing an already frozen object. 23 | # 24 | # @example EnforcedStyle: literals (default) 25 | # # bad 26 | # class Model 27 | # @list = [1, 2, 3] 28 | # end 29 | # 30 | # # good 31 | # class Model 32 | # @list = [1, 2, 3].freeze 33 | # end 34 | # 35 | # # good 36 | # class Model 37 | # @var = <<~TESTING.freeze 38 | # This is a heredoc 39 | # TESTING 40 | # end 41 | # 42 | # # good 43 | # class Model 44 | # @var = Something.new 45 | # end 46 | # 47 | # @example EnforcedStyle: strict 48 | # # bad 49 | # class Model 50 | # @var = Something.new 51 | # end 52 | # 53 | # # bad 54 | # class Model 55 | # @var = Struct.new do 56 | # def foo 57 | # puts 1 58 | # end 59 | # end 60 | # end 61 | # 62 | # # good 63 | # class Model 64 | # @var = Something.new.freeze 65 | # end 66 | # 67 | # # good 68 | # class Model 69 | # @var = Struct.new do 70 | # def foo 71 | # puts 1 72 | # end 73 | # end.freeze 74 | # end 75 | class MutableClassInstanceVariable < Base 76 | extend AutoCorrector 77 | 78 | include FrozenStringLiteral 79 | include ConfigurableEnforcedStyle 80 | include OperationWithThreadsafeResult 81 | 82 | MSG = 'Freeze mutable objects assigned to class instance variables.' 83 | FROZEN_STRING_LITERAL_TYPES_RUBY27 = %i[str dstr].freeze 84 | FROZEN_STRING_LITERAL_TYPES_RUBY30 = %i[str].freeze 85 | 86 | def on_ivasgn(node) 87 | return unless in_class?(node) 88 | 89 | on_assignment(node.expression) 90 | end 91 | 92 | def on_or_asgn(node) 93 | return unless node.assignment_node.ivasgn_type? 94 | return unless in_class?(node) 95 | 96 | on_assignment(node.expression) 97 | end 98 | 99 | def on_masgn(node) 100 | return unless in_class?(node) 101 | 102 | mlhs, values = *node # rubocop:disable InternalAffairs/NodeDestructuring 103 | return unless values.array_type? 104 | 105 | mlhs.to_a.zip(values.to_a).each do |lhs, value| 106 | next unless lhs.ivasgn_type? 107 | 108 | on_assignment(value) 109 | end 110 | end 111 | 112 | def autocorrect(corrector, node) 113 | expr = node.source_range 114 | 115 | splat_value = splat_value(node) 116 | if splat_value 117 | correct_splat_expansion(corrector, expr, splat_value) 118 | elsif node.array_type? && !node.bracketed? 119 | corrector.insert_before(expr, '[') 120 | corrector.insert_after(expr, ']') 121 | elsif requires_parentheses?(node) 122 | corrector.insert_before(expr, '(') 123 | corrector.insert_after(expr, ')') 124 | end 125 | 126 | corrector.insert_after(expr, '.freeze') 127 | end 128 | 129 | private 130 | 131 | def frozen_string_literal?(node) 132 | literal_types = if target_ruby_version >= 3.0 133 | FROZEN_STRING_LITERAL_TYPES_RUBY30 134 | else 135 | FROZEN_STRING_LITERAL_TYPES_RUBY27 136 | end 137 | literal_types.include?(node.type) && frozen_string_literals_enabled? 138 | end 139 | 140 | def on_assignment(value) 141 | if style == :strict 142 | strict_check(value) 143 | else 144 | check(value) 145 | end 146 | end 147 | 148 | def strict_check(value) 149 | return if immutable_literal?(value) 150 | return if operation_produces_immutable_object?(value) 151 | return if operation_produces_threadsafe_object?(value) 152 | return if frozen_string_literal?(value) 153 | 154 | add_offense(value) do |corrector| 155 | autocorrect(corrector, value) 156 | end 157 | end 158 | 159 | def check(value) 160 | return unless mutable_literal?(value) || 161 | range_enclosed_in_parentheses?(value) 162 | return if frozen_string_literal?(value) 163 | 164 | add_offense(value) do |corrector| 165 | autocorrect(corrector, value) 166 | end 167 | end 168 | 169 | def in_class?(node) 170 | container = node.ancestors.find do |ancestor| 171 | container?(ancestor) 172 | end 173 | return false if container.nil? 174 | 175 | %i[class module].include?(container.type) 176 | end 177 | 178 | def container?(node) 179 | return true if define_singleton_method?(node) 180 | return true if define_method?(node) 181 | 182 | %i[def defs class module].include?(node.type) 183 | end 184 | 185 | def mutable_literal?(node) 186 | return false if node.nil? 187 | 188 | node.mutable_literal? || range_type?(node) 189 | end 190 | 191 | def immutable_literal?(node) 192 | node.nil? || node.immutable_literal? 193 | end 194 | 195 | def requires_parentheses?(node) 196 | range_type?(node) || 197 | (node.send_type? && node.loc.dot.nil?) 198 | end 199 | 200 | def range_type?(node) 201 | node.type?(:range) 202 | end 203 | 204 | def correct_splat_expansion(corrector, expr, splat_value) 205 | if range_enclosed_in_parentheses?(splat_value) 206 | corrector.replace(expr, "#{splat_value.source}.to_a") 207 | else 208 | corrector.replace(expr, "(#{splat_value.source}).to_a") 209 | end 210 | end 211 | 212 | # @!method define_singleton_method?(node) 213 | def_node_matcher :define_singleton_method?, <<~PATTERN 214 | (block (send nil? :define_singleton_method ...) ...) 215 | PATTERN 216 | 217 | # @!method define_method?(node) 218 | def_node_matcher :define_method?, <<~PATTERN 219 | (block (send nil? :define_method ...) ...) 220 | PATTERN 221 | 222 | # @!method splat_value(node) 223 | def_node_matcher :splat_value, <<~PATTERN 224 | (array (splat $_)) 225 | PATTERN 226 | 227 | # NOTE: Some of these patterns may not actually return an immutable 228 | # object but we will consider them immutable for this cop. 229 | # @!method operation_produces_immutable_object?(node) 230 | def_node_matcher :operation_produces_immutable_object?, <<~PATTERN 231 | { 232 | (const _ _) 233 | (send (const {nil? cbase} :Struct) :new ...) 234 | (block (send (const {nil? cbase} :Struct) :new ...) ...) 235 | (send _ :freeze) 236 | (send {float int} {:+ :- :* :** :/ :% :<<} _) 237 | (send _ {:+ :- :* :** :/ :%} {float int}) 238 | (send _ {:== :=== :!= :<= :>= :< :>} _) 239 | (send (const {nil? cbase} :ENV) :[] _) 240 | (or (send (const {nil? cbase} :ENV) :[] _) _) 241 | (send _ {:count :length :size} ...) 242 | (block (send _ {:count :length :size} ...) ...) 243 | } 244 | PATTERN 245 | 246 | # @!method range_enclosed_in_parentheses?(node) 247 | def_node_matcher :range_enclosed_in_parentheses?, <<~PATTERN 248 | (begin (range _ _)) 249 | PATTERN 250 | end 251 | end 252 | end 253 | end 254 | -------------------------------------------------------------------------------- /lib/rubocop/cop/thread_safety/new_thread.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | module Cop 5 | module ThreadSafety 6 | # Avoid starting new threads. 7 | # 8 | # Let a framework like Sidekiq handle the threads. 9 | # 10 | # @example 11 | # # bad 12 | # Thread.new { do_work } 13 | class NewThread < Base 14 | MSG = 'Avoid starting new threads.' 15 | RESTRICT_ON_SEND = %i[new fork start].freeze 16 | 17 | # @!method new_thread?(node) 18 | def_node_matcher :new_thread?, <<~MATCHER 19 | (call (const {nil? cbase} :Thread) {:new :fork :start} ...) 20 | MATCHER 21 | 22 | def on_send(node) 23 | new_thread?(node) { add_offense(node) } 24 | end 25 | alias on_csend on_send 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/rubocop/cop/thread_safety/rack_middleware_instance_variable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | module Cop 5 | module ThreadSafety 6 | # Avoid instance variables in rack middleware. 7 | # 8 | # Middlewares are initialized once, meaning any instance variables are shared between executor threads. 9 | # To avoid potential race conditions, it's recommended to design middlewares to be stateless 10 | # or to implement proper synchronization mechanisms. 11 | # 12 | # @example 13 | # # bad 14 | # class CounterMiddleware 15 | # def initialize(app) 16 | # @app = app 17 | # @counter = 0 18 | # end 19 | # 20 | # def call(env) 21 | # app.call(env) 22 | # ensure 23 | # @counter += 1 24 | # end 25 | # end 26 | # 27 | # # good 28 | # class CounterMiddleware 29 | # def initialize(app) 30 | # @app = app 31 | # @counter = Concurrent::AtomicReference.new(0) 32 | # end 33 | # 34 | # def call(env) 35 | # app.call(env) 36 | # ensure 37 | # @counter.update { |ref| ref + 1 } 38 | # end 39 | # end 40 | # 41 | # class IdentityMiddleware 42 | # def initialize(app) 43 | # @app = app 44 | # end 45 | # 46 | # def call(env) 47 | # app.call(env) 48 | # end 49 | # end 50 | class RackMiddlewareInstanceVariable < Base 51 | include AllowedIdentifiers 52 | include OperationWithThreadsafeResult 53 | 54 | MSG = 'Avoid instance variables in Rack middleware.' 55 | 56 | RESTRICT_ON_SEND = %i[instance_variable_get instance_variable_set].freeze 57 | 58 | # @!method rack_middleware_like_class?(node) 59 | def_node_matcher :rack_middleware_like_class?, <<~MATCHER 60 | (class (const nil? _) nil? (begin <(def :initialize (args (arg _)+) ...) (def :call (args (arg _)) ...) ...>)) 61 | MATCHER 62 | 63 | # @!method app_variable(node) 64 | def_node_search :app_variable, <<~MATCHER 65 | (def :initialize (args (arg $_) ...) `(ivasgn $_ (lvar $_))) 66 | MATCHER 67 | 68 | def on_class(node) 69 | return unless rack_middleware_like_class?(node) 70 | 71 | constructor_method = find_constructor_method(node) 72 | return unless (application_variable = extract_application_variable_from_contructor_method(constructor_method)) 73 | 74 | safe_variables = extract_safe_variables_from_constructor_method(constructor_method) 75 | 76 | node.each_node(:def) do |def_node| 77 | def_node.each_node(:ivasgn, :ivar) do |ivar_node| 78 | variable, = ivar_node.to_a 79 | if variable == application_variable || safe_variables.include?(variable) || allowed_identifier?(variable) 80 | next 81 | end 82 | 83 | add_offense ivar_node 84 | end 85 | end 86 | end 87 | 88 | def on_send(node) 89 | argument = node.first_argument 90 | 91 | return unless argument&.type?(:sym, :str) 92 | return if allowed_identifier?(argument.value) 93 | 94 | add_offense node 95 | end 96 | alias on_csend on_send 97 | 98 | private 99 | 100 | def find_constructor_method(class_node) 101 | class_node 102 | .each_node(:def) 103 | .find { |node| node.method?(:initialize) && node.arguments.size >= 1 } 104 | end 105 | 106 | def extract_application_variable_from_contructor_method(constructor_method) 107 | constructor_method 108 | .then { |node| app_variable(node) } 109 | .then { |variables| variables.first[1] if variables.first } 110 | end 111 | 112 | def extract_safe_variables_from_constructor_method(constructor_method) 113 | constructor_method 114 | .each_node(:ivasgn) 115 | .select { |ivasgn_node| operation_produces_threadsafe_object?(ivasgn_node.to_a[1]) } 116 | .map { _1.to_a[0] } 117 | end 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/rubocop/thread_safety.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | # RuboCop::ThreadSafety detects some potential thread safety issues. 5 | module ThreadSafety 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/rubocop/thread_safety/plugin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'lint_roller' 4 | 5 | module RuboCop 6 | module ThreadSafety 7 | # A plugin that integrates RuboCop ThreadSafety with RuboCop's plugin system. 8 | class Plugin < LintRoller::Plugin 9 | # :nocov: 10 | def about 11 | LintRoller::About.new( 12 | name: 'rubocop-thread_safety', 13 | version: Version::STRING, 14 | homepage: 'https://github.com/rubocop/rubocop-thread_safety', 15 | description: 'Thread-safety checks via static analysis.' 16 | ) 17 | end 18 | # :nocov: 19 | 20 | def supported?(context) 21 | context.engine == :rubocop 22 | end 23 | 24 | def rules(_context) 25 | project_root = Pathname.new(__dir__).join('../../..') 26 | 27 | obsoletion = project_root.join('config', 'obsoletion.yml') 28 | ConfigObsoletion.files << obsoletion 29 | 30 | LintRoller::Rules.new( 31 | type: :path, 32 | config_format: :rubocop, 33 | value: project_root.join('config/default.yml') 34 | ) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/rubocop/thread_safety/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | module ThreadSafety 5 | module Version 6 | STRING = '0.7.2' 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /rubocop-thread_safety.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'rubocop/thread_safety/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'rubocop-thread_safety' 9 | spec.version = RuboCop::ThreadSafety::Version::STRING 10 | spec.authors = ['Michael Gee'] 11 | spec.email = ['michaelpgee@gmail.com'] 12 | 13 | spec.summary = 'Thread-safety checks via static analysis' 14 | spec.description = <<-DESCRIPTION 15 | Thread-safety checks via static analysis. 16 | A plugin for the RuboCop code style enforcing & linting tool. 17 | DESCRIPTION 18 | spec.homepage = 'https://github.com/rubocop/rubocop-thread_safety' 19 | spec.licenses = ['MIT'] 20 | 21 | spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| 22 | ls.readlines("\x0", chomp: true).reject do |f| 23 | (f == File.basename(__FILE__)) || f.start_with?( 24 | *%w[ 25 | bin/ spec/ .git .github/ bin/ docs/ gemfiles/ tasks/ 26 | Gemfile Appraisals .rspec .rubocop.yml .yamllint.yml Rakefile 27 | ] 28 | ) 29 | end 30 | end 31 | 32 | spec.metadata = { 33 | 'changelog_uri' => 'https://github.com/rubocop/rubocop-thread_safety/blob/master/CHANGELOG.md', 34 | 'source_code_uri' => 'https://github.com/rubocop/rubocop-thread_safety', 35 | 'bug_tracker_uri' => 'https://github.com/rubocop/rubocop-thread_safety/issues', 36 | 'rubygems_mfa_required' => 'true', 37 | 'default_lint_roller_plugin' => 'RuboCop::ThreadSafety::Plugin' 38 | } 39 | 40 | spec.bindir = 'exe' 41 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 42 | spec.require_paths = ['lib'] 43 | 44 | spec.required_ruby_version = '>= 2.7.0' 45 | 46 | spec.add_dependency 'lint_roller', '~> 1.1' 47 | spec.add_dependency 'rubocop', '~> 1.72', '>= 1.72.1' 48 | end 49 | -------------------------------------------------------------------------------- /spec/license_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'the LICENSE' do # rubocop:disable RSpec/DescribeClass 6 | let(:license) { Pathname('LICENSE.txt') } 7 | 8 | it 'exists' do 9 | expect(license).to exist 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/rubocop/cop/thread_safety/class_and_module_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RuboCop::Cop::ThreadSafety::ClassAndModuleAttributes, :config do 4 | let(:msg) { 'Avoid mutating class and module attributes.' } 5 | 6 | context 'when in the singleton class' do 7 | it 'registers an offense for `attr`' do 8 | expect_offense(<<~RUBY) 9 | module Test 10 | class << self 11 | attr :foobar 12 | ^^^^^^^^^^^^ #{msg} 13 | end 14 | end 15 | RUBY 16 | end 17 | 18 | it 'registers an offense for `attr_accessor`' do 19 | expect_offense(<<~RUBY) 20 | module Test 21 | class << self 22 | attr_accessor :foobar 23 | ^^^^^^^^^^^^^^^^^^^^^ #{msg} 24 | end 25 | end 26 | RUBY 27 | end 28 | 29 | it 'registers an offense for `attr_writer`' do 30 | expect_offense(<<~RUBY) 31 | module Test 32 | class << self 33 | attr_writer :foobar 34 | ^^^^^^^^^^^^^^^^^^^ #{msg} 35 | end 36 | end 37 | RUBY 38 | end 39 | 40 | it 'registers no offense for `attr_reader`' do 41 | expect_no_offenses(<<~RUBY) 42 | module Test 43 | class << self 44 | attr_reader :foobar 45 | end 46 | end 47 | RUBY 48 | end 49 | 50 | it 'registers an offense for `attr_internal`' do 51 | expect_offense(<<~RUBY) 52 | module Test 53 | class << self 54 | attr_internal :foobar 55 | ^^^^^^^^^^^^^^^^^^^^^ #{msg} 56 | end 57 | end 58 | RUBY 59 | end 60 | 61 | it 'registers an offense for `attr_internal_accessor`' do 62 | expect_offense(<<~RUBY) 63 | module Test 64 | class << self 65 | attr_internal_accessor :foobar 66 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 67 | end 68 | end 69 | RUBY 70 | end 71 | 72 | it 'registers an offense for `attr_internal_writer`' do 73 | expect_offense(<<~RUBY) 74 | module Test 75 | class << self 76 | attr_internal_writer :foobar 77 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 78 | end 79 | end 80 | RUBY 81 | end 82 | 83 | it 'registers no offense for `attr_internal_reader`' do 84 | expect_no_offenses(<<~RUBY) 85 | module Test 86 | class << self 87 | attr_internal_reader :foobar 88 | end 89 | end 90 | RUBY 91 | end 92 | 93 | it 'registers no offense for `attr_accessor` within procs' do 94 | expect_no_offenses(<<~RUBY) 95 | module Test 96 | DEFINE_ACCESSOR = ->{ attr_accessor :foobar } 97 | end 98 | RUBY 99 | end 100 | 101 | context 'when in a singleton class method' do 102 | it 'registers no offense for `attr_accessor`' do 103 | expect_no_offenses(<<~RUBY) 104 | module Test 105 | class << self 106 | def add_foobar_attr 107 | attr_accessor :foobar 108 | end 109 | end 110 | end 111 | RUBY 112 | end 113 | end 114 | end 115 | 116 | it 'registers an offense for `mattr_writer`' do 117 | expect_offense(<<~RUBY) 118 | module Test 119 | mattr_writer :foobar 120 | ^^^^^^^^^^^^^^^^^^^^ #{msg} 121 | end 122 | RUBY 123 | end 124 | 125 | it 'registers an offense for `mattr_accessor`' do 126 | expect_offense(<<~RUBY) 127 | module Test 128 | mattr_accessor :foobar 129 | ^^^^^^^^^^^^^^^^^^^^^^ #{msg} 130 | end 131 | RUBY 132 | end 133 | 134 | it 'registers an offense for `cattr_writer`' do 135 | expect_offense(<<~RUBY) 136 | class Test 137 | cattr_writer :foobar 138 | ^^^^^^^^^^^^^^^^^^^^ #{msg} 139 | end 140 | RUBY 141 | end 142 | 143 | it 'registers an offense for `cattr_accessor`' do 144 | expect_offense(<<~RUBY) 145 | class Test 146 | cattr_accessor :foobar 147 | ^^^^^^^^^^^^^^^^^^^^^^ #{msg} 148 | end 149 | RUBY 150 | end 151 | 152 | it 'registers an offense for `class_attribute`' do 153 | expect_offense(<<~RUBY) 154 | class Test 155 | class_attribute :foobar 156 | ^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 157 | end 158 | RUBY 159 | end 160 | 161 | context 'with `ActiveSupportClassAttributeAllowed` option set to `true`' do 162 | let(:cop_config) { { 'ActiveSupportClassAttributeAllowed' => true } } 163 | 164 | it 'does not register an offense for `class_attribute`' do 165 | expect_no_offenses(<<~RUBY) 166 | class Test 167 | class_attribute :foobar 168 | end 169 | RUBY 170 | end 171 | end 172 | 173 | it 'registers no offense for other class macro calls' do 174 | expect_no_offenses(<<~RUBY) 175 | class Test 176 | belongs_to :foobar 177 | end 178 | RUBY 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /spec/rubocop/cop/thread_safety/class_instance_variable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RuboCop::Cop::ThreadSafety::ClassInstanceVariable, :config do 4 | let(:msg) { 'Avoid class instance variables.' } 5 | 6 | it 'registers an offense for assigning to an ivar in a class method' do 7 | expect_offense(<<~RUBY) 8 | class Test 9 | def self.some_method(params) 10 | @params = params 11 | ^^^^^^^ #{msg} 12 | end 13 | end 14 | RUBY 15 | end 16 | 17 | it 'registers no offense when the assignment is synchronized by a mutex' do 18 | expect_no_offenses(<<~RUBY) 19 | class Test 20 | SEMAPHORE = Mutex.new 21 | def self.some_method(params) 22 | SEMAPHORE.synchronize do 23 | @params = params 24 | end 25 | end 26 | end 27 | RUBY 28 | end 29 | 30 | it 'registers no offense when memoization is synchronized by a mutex' do 31 | expect_no_offenses(<<~RUBY) 32 | class Test 33 | SEMAPHORE = Mutex.new 34 | def self.types 35 | SEMAPHORE 36 | .synchronize { @all_types ||= type_class.all } 37 | end 38 | end 39 | RUBY 40 | end 41 | 42 | it 'registers no offense for assigning an ivar in define_method' do 43 | expect_no_offenses(<<~RUBY) 44 | class Test 45 | def self.factory_method 46 | define_method(:some_method) do |params| 47 | @params = params 48 | end 49 | end 50 | end 51 | RUBY 52 | end 53 | 54 | it 'registers no offense for assigning an ivar in `Struct` scope' do 55 | expect_no_offenses(<<~RUBY) 56 | class Test 57 | def self.factory_method 58 | Struct.new(:width, :height) do 59 | def area 60 | @area ||= width * height 61 | end 62 | end 63 | end 64 | end 65 | RUBY 66 | end 67 | 68 | it 'registers no offense for assigning an ivar in `Data` scope' do 69 | expect_no_offenses(<<~RUBY) 70 | class Test 71 | def self.factory_method 72 | Data.define(:width, :height) do 73 | def area 74 | @area ||= width * height 75 | end 76 | end 77 | end 78 | end 79 | RUBY 80 | end 81 | 82 | it 'registers no offense for assigning an ivar in `Class` scope' do 83 | expect_no_offenses(<<~RUBY) 84 | class Test 85 | def self.factory_method 86 | Class.new do 87 | def area 88 | @area ||= some_computation 89 | end 90 | end 91 | end 92 | end 93 | RUBY 94 | end 95 | 96 | it 'registers no offense for `instance_variable_get` in new lexical scope' do 97 | expect_no_offenses(<<~RUBY) 98 | class Test 99 | def self.factory_method 100 | Class.new do 101 | def area 102 | instance_variable_get(:@area) 103 | end 104 | end 105 | end 106 | end 107 | RUBY 108 | end 109 | 110 | it 'registers an offense for reading an ivar in a nested class method' do # rubocop:disable RSpec/ExampleLength 111 | expect_offense(<<~RUBY) 112 | class Test 113 | define_method :generate_new_class do 114 | Class.new do 115 | def self.area 116 | @area ||= some_computation 117 | ^^^^^ #{msg} 118 | end 119 | end 120 | end 121 | end 122 | RUBY 123 | end 124 | 125 | it 'registers an offense for reading an ivar in a class method' do 126 | expect_offense(<<~RUBY) 127 | class Test 128 | def self.some_method 129 | do_work(@params) 130 | ^^^^^^^ #{msg} 131 | end 132 | end 133 | RUBY 134 | end 135 | 136 | it 'registers an offense for assigning an ivar in module ClassMethods' do 137 | expect_offense(<<~RUBY) 138 | module ClassMethods 139 | def some_method(params) 140 | @params = params 141 | ^^^^^^^ #{msg} 142 | end 143 | end 144 | RUBY 145 | end 146 | 147 | it 'registers an offense for assigning an ivar in class_methods' do 148 | expect_offense(<<~RUBY) 149 | module Test 150 | class_methods do 151 | def some_method(params) 152 | @params = params 153 | ^^^^^^^ #{msg} 154 | end 155 | end 156 | end 157 | RUBY 158 | end 159 | 160 | # rubocop:disable RSpec/ExampleLength 161 | it 'registers an offense for assigning an ivar in class_methods within lambda', :with_legacy_lambda_node do 162 | expect_offense(<<~RUBY) 163 | module Test 164 | class_methods do 165 | ->() { 166 | def some_method(params) 167 | @params = params 168 | ^^^^^^^ #{msg} 169 | end 170 | } 171 | end 172 | end 173 | RUBY 174 | end 175 | # rubocop:enable RSpec/ExampleLength 176 | 177 | it 'registers an offense for assigning an ivar in a class singleton method' do 178 | expect_offense(<<~RUBY) 179 | class Test 180 | class << self 181 | def some_method(params) 182 | @params = params 183 | ^^^^^^^ #{msg} 184 | end 185 | end 186 | end 187 | RUBY 188 | end 189 | 190 | it 'registers an offense for assigning an ivar in define_singleton_method' do 191 | expect_offense(<<~RUBY) 192 | class Test 193 | define_singleton_method(:some_method) do |params| 194 | @params = params 195 | ^^^^^^^ #{msg} 196 | end 197 | end 198 | RUBY 199 | end 200 | 201 | it 'registers an offense for ivar_get in a class method' do 202 | expect_offense(<<~RUBY) 203 | class Test 204 | def self.some_method 205 | do_work(instance_variable_get(:@params)) 206 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 207 | end 208 | end 209 | RUBY 210 | end 211 | 212 | it 'registers an offense for ivar_set in a class singleton method' do 213 | expect_offense(<<~RUBY) 214 | class Test 215 | class << self 216 | def some_method(name, params) 217 | instance_variable_set(:"@\#{name}", params) 218 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 219 | end 220 | end 221 | end 222 | RUBY 223 | end 224 | 225 | it 'registers an offense for ivar_set in module ClassMethods' do 226 | expect_offense(<<~RUBY) 227 | module ClassMethods 228 | def some_method(params) 229 | instance_variable_set(:@params, params) 230 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 231 | end 232 | end 233 | RUBY 234 | end 235 | 236 | it 'registers an offense for ivar_set in class_methods' do 237 | expect_offense(<<~RUBY) 238 | module Test 239 | class_methods do 240 | def some_method(params) 241 | instance_variable_set(:@params, params) 242 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 243 | end 244 | end 245 | end 246 | RUBY 247 | end 248 | 249 | it 'registers an offense for ivar_set in define_singleton_method' do 250 | expect_offense(<<~RUBY) 251 | class Test 252 | define_singleton_method(:some_method) do |params| 253 | instance_variable_set(:@params, params) 254 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 255 | end 256 | end 257 | RUBY 258 | end 259 | 260 | it 'registers an offense for ivar_set in a method below module_function directive' do 261 | expect_offense(<<~RUBY) 262 | module Test 263 | module_function 264 | 265 | def some_method(params) 266 | instance_variable_set(:@params, params) 267 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 268 | end 269 | end 270 | RUBY 271 | end 272 | 273 | it 'registers an offense for ivar_set in a method marked by module_function' do 274 | expect_offense(<<~RUBY) 275 | module Test 276 | def some_method(params) 277 | instance_variable_set(:@params, params) 278 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 279 | end 280 | 281 | module_function :some_method 282 | end 283 | RUBY 284 | end 285 | 286 | it 'does not register an offenses for synchronized ivar_set in a method marked by module_function' do 287 | expect_no_offenses(<<~RUBY) 288 | module Test 289 | def some_method(params) 290 | $mutex.synchronize do 291 | instance_variable_set(:@params, params) 292 | end 293 | end 294 | 295 | module_function :some_method 296 | end 297 | RUBY 298 | end 299 | 300 | it 'registers an offense for assigning an ivar in a method below module_function directive' do 301 | expect_offense(<<~RUBY) 302 | module Test 303 | module_function 304 | 305 | def some_method(params) 306 | @params = params 307 | ^^^^^^^ #{msg} 308 | end 309 | end 310 | RUBY 311 | end 312 | 313 | it 'registers an offense for assigning an ivar in a method marked by module_function' do 314 | expect_offense(<<~RUBY) 315 | module Test 316 | def some_method(params) 317 | @params = params 318 | ^^^^^^^ #{msg} 319 | end 320 | 321 | module_function :some_method 322 | end 323 | RUBY 324 | end 325 | 326 | it 'registers an offense for instance variable within `class_eval` block' do # rubocop:disable RSpec/ExampleLength 327 | expect_offense(<<~RUBY) 328 | def separate_with(separator) 329 | Example.class_eval do 330 | @separator = separator 331 | ^^^^^^^^^^ #{msg} 332 | end 333 | end 334 | RUBY 335 | 336 | expect_offense(<<~RUBY) 337 | def separate_with(separator) 338 | ::Example.class_eval do 339 | @separator = separator 340 | ^^^^^^^^^^ #{msg} 341 | end 342 | end 343 | RUBY 344 | end 345 | 346 | it 'registers an offense for instance variable within `class_exec` block' do 347 | expect_offense(<<~RUBY) 348 | def separate_with(separator) 349 | Example.class_exec do 350 | @separator = separator 351 | ^^^^^^^^^^ #{msg} 352 | end 353 | end 354 | RUBY 355 | end 356 | 357 | it 'registers no offense for ivar_set in define_method' do 358 | expect_no_offenses(<<~RUBY) 359 | class Test 360 | def self.factory_method 361 | define_method(:some_method) do |params| 362 | instance_variable_set(:@params, params) 363 | end 364 | end 365 | end 366 | RUBY 367 | end 368 | 369 | it 'registers no offense for using ivar_get on object in a class method' do 370 | expect_no_offenses(<<~RUBY) 371 | class Test 372 | def self.some_method(obj, params) 373 | obj.instance_variable_get(:@params) 374 | end 375 | end 376 | RUBY 377 | end 378 | 379 | it 'registers no offense for using ivar_set on object in a class method' do 380 | expect_no_offenses(<<~RUBY) 381 | class Test 382 | class << self 383 | def some_method(obj, params) 384 | obj.instance_variable_set(:@params, params) 385 | end 386 | end 387 | end 388 | RUBY 389 | end 390 | 391 | it 'registers no offense for using an ivar in an instance method' do 392 | expect_no_offenses(<<~RUBY) 393 | class Test 394 | def some_method(params) 395 | @params = params 396 | do_work(@params) 397 | end 398 | end 399 | RUBY 400 | end 401 | 402 | it 'registers no offense for using ivar methods in an instance method' do 403 | expect_no_offenses(<<~RUBY) 404 | class Test 405 | def some_method(params) 406 | instance_variable_set(:@params, params) 407 | do_work(instance_variable_get(:@params)) 408 | end 409 | end 410 | RUBY 411 | end 412 | 413 | it 'registers no offense for using an ivar in a module below ClassMethods' do 414 | expect_no_offenses(<<~RUBY) 415 | module ClassMethods 416 | module Other 417 | def some_method(params) 418 | @params = params 419 | end 420 | end 421 | end 422 | RUBY 423 | end 424 | 425 | it 'registers no offense for assigning an ivar in a method above module_function directive' do 426 | expect_no_offenses(<<~RUBY) 427 | module Test 428 | def some_method(params) 429 | @params = params 430 | end 431 | 432 | module_function 433 | end 434 | RUBY 435 | end 436 | 437 | it 'registers no offense for assigning an ivar in a method not marked by module_function' do 438 | expect_no_offenses(<<~RUBY) 439 | module Test 440 | def some_method(params) 441 | @params = params 442 | end 443 | 444 | def another_method(params) 445 | puts params 446 | end 447 | 448 | module_function :another_method 449 | end 450 | RUBY 451 | end 452 | 453 | it 'does not register an offense for instance variable within `module_eval` block' do 454 | expect_no_offenses(<<~RUBY) 455 | def separate_with(separator) 456 | Utilities.module_eval do 457 | @separator = separator 458 | end 459 | end 460 | RUBY 461 | end 462 | 463 | it 'does not register an offense for instance variable within `module_exec` block' do 464 | expect_no_offenses(<<~RUBY) 465 | def separate_with(separator) 466 | Utilities.module_exec do 467 | @separator = separator 468 | end 469 | end 470 | RUBY 471 | end 472 | 473 | it 'does not register an offense for instance variable within `class_*` with new instance method' do 474 | expect_no_offenses(<<~RUBY) 475 | def separate_with(separator) 476 | Example.class_eval do 477 | def separator 478 | @separator 479 | end 480 | end 481 | end 482 | RUBY 483 | end 484 | 485 | it 'does not register an offense for instance variable within `class_*` with string argument' do 486 | expect_no_offenses(<<~RUBY) 487 | def separate_with(separator) 488 | Example.class_eval "@f = Kernel.exit" 489 | end 490 | RUBY 491 | end 492 | 493 | context 'with `ActionDispatch` callbacks' do 494 | %i[ 495 | prepend_around_action 496 | prepend_before_action 497 | before_action 498 | append_before_action 499 | around_action 500 | append_around_action 501 | append_after_action 502 | after_action 503 | prepend_after_action 504 | ].each do |action_dispatch_callback_name| 505 | it "registers no offense for `#{action_dispatch_callback_name}` callback" do 506 | expect_no_offenses(<<~RUBY) 507 | def self.foo 508 | #{action_dispatch_callback_name} do 509 | @language = :haskell 510 | end 511 | end 512 | RUBY 513 | end 514 | end 515 | end 516 | end 517 | -------------------------------------------------------------------------------- /spec/rubocop/cop/thread_safety/dir_chdir_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RuboCop::Cop::ThreadSafety::DirChdir, :config do 4 | %w[Dir.chdir Dir&.chdir FileUtils.chdir FileUtils.cd].each do |expression| 5 | context "with `#{expression}` call" do 6 | it 'registers an offense' do 7 | expect_offense(<<~RUBY, expression: expression) 8 | %{expression}("/var/run") 9 | ^{expression}^^^^^^^^^^^^ Avoid using `%{expression}` due to its process-wide effect. 10 | RUBY 11 | end 12 | 13 | it 'registers an offense without arguments' do 14 | expect_offense(<<~RUBY, expression: expression) 15 | %{expression} 16 | ^{expression} Avoid using `%{expression}` due to its process-wide effect. 17 | RUBY 18 | end 19 | 20 | it 'registers an offense with fully quialified constant name' do 21 | expect_offense(<<~RUBY, expression: expression) 22 | ::%{expression}("/var/run") 23 | ^{expression}^^^^^^^^^^^^^^ Avoid using `%{expression}` due to its process-wide effect. 24 | RUBY 25 | end 26 | 27 | it 'registers an offense with provided block' do 28 | expect_offense(<<~RUBY, expression: expression) 29 | %{expression}("/var/run") do 30 | ^{expression}^^^^^^^^^^^^ Avoid using `%{expression}` due to its process-wide effect. 31 | puts Dir.pwd 32 | end 33 | RUBY 34 | end 35 | 36 | it 'registers an offense with provided block with argument' do 37 | expect_offense(<<~RUBY, expression: expression) 38 | %{expression}("/var/run") do |dir| 39 | ^{expression}^^^^^^^^^^^^ Avoid using `%{expression}` due to its process-wide effect. 40 | puts dir 41 | end 42 | RUBY 43 | end 44 | 45 | it 'registers an offense with provided block argument' do 46 | expect_offense(<<~RUBY, expression: expression) 47 | def change_dir(&block) 48 | %{expression}("/var/run", &block) 49 | ^{expression}^^^^^^^^^^^^^^^^^^^^ Avoid using `%{expression}` due to its process-wide effect. 50 | end 51 | RUBY 52 | end 53 | end 54 | end 55 | 56 | %w[Dir FileUtils].each do |constant_name| 57 | it "does not register an offense for unrelated `#{constant_name}` method" do 58 | expect_no_offenses(<<~RUBY) 59 | #{constant_name}.pwd 60 | RUBY 61 | end 62 | end 63 | 64 | %w[chdir cd].each do |method_name| 65 | it "does not register an offense for unrelated `#{method_name}` with unrelated receiver" do 66 | expect_no_offenses(<<~RUBY) 67 | Foo.#{method_name} 68 | RUBY 69 | 70 | expect_no_offenses(<<~RUBY) 71 | foo.#{method_name} 72 | RUBY 73 | 74 | expect_no_offenses(<<~RUBY) 75 | #{method_name} 76 | RUBY 77 | end 78 | end 79 | 80 | context 'with `AllowCallWithBlock` configuration option set to `true`' do 81 | let(:cop_config) do 82 | { 'Enabled' => true, 'AllowCallWithBlock' => true } 83 | end 84 | 85 | %w[Dir.chdir FileUtils.chdir FileUtils.cd].each do |expression| 86 | it 'registers an offense for block-less call' do 87 | expect_offense(<<~RUBY, expression: expression) 88 | %{expression}("/var/run") 89 | ^{expression}^^^^^^^^^^^^ Avoid using `%{expression}` due to its process-wide effect. 90 | RUBY 91 | end 92 | 93 | it 'does not register an offense for block-ful call' do 94 | expect_no_offenses(<<~RUBY) 95 | #{expression}("/var/run") do 96 | p Dir.pwd 97 | end 98 | RUBY 99 | end 100 | 101 | it 'does no register an offense with provided block with argument' do 102 | expect_no_offenses(<<~RUBY) 103 | #{expression}("/var/run") do |dir| 104 | p dir 105 | end 106 | RUBY 107 | end 108 | 109 | it 'does not register an offense with provided block argument' do 110 | expect_no_offenses(<<~RUBY) 111 | def change_dir(&block) 112 | #{expression}("/var/run", &block) 113 | end 114 | RUBY 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/rubocop/cop/thread_safety/mutable_class_instance_variable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # rubocop:disable RSpec/ExampleLength,RSpec/MultipleMemoizedHelpers,RSpec/NestedGroups 4 | RSpec.describe RuboCop::Cop::ThreadSafety::MutableClassInstanceVariable, 5 | :config do 6 | let(:msg) { 'Freeze mutable objects assigned to class instance variables.' } 7 | let(:prefix) { nil } 8 | let(:suffix) { nil } 9 | let(:indent) { '' } 10 | 11 | def surround(code) 12 | [ 13 | prefix, 14 | code.split("\n").map { |line| "#{indent}#{line}" }, 15 | suffix 16 | ].compact.join("\n") 17 | end 18 | 19 | shared_examples 'mutable objects' do |o| 20 | context 'when assigning with =' do 21 | it "registers an offense for #{o} assigned to a class ivar" do 22 | expect_offense(surround(<<~RUBY), o: o) 23 | @var = %{o} 24 | ^{o} #{msg} 25 | RUBY 26 | 27 | expect_correction(surround(<<~RUBY)) 28 | @var = #{o}.freeze 29 | RUBY 30 | end 31 | end 32 | 33 | context 'when assigning with ||=' do 34 | it "registers an offense for #{o} assigned to a class ivar" do 35 | expect_offense(surround(<<~RUBY), o: o) 36 | @var ||= %{o} 37 | ^{o} #{msg} 38 | RUBY 39 | 40 | expect_correction(surround(<<~RUBY)) 41 | @var ||= #{o}.freeze 42 | RUBY 43 | end 44 | 45 | it 'registers no offense for `or-asgn` node without ivar' do 46 | expect_no_offenses(surround(<<~RUBY)) 47 | var ||= %{o} 48 | RUBY 49 | end 50 | end 51 | end 52 | 53 | shared_examples 'immutable objects' do |o| 54 | it "allows #{o} to be assigned to a class ivar" do 55 | expect_no_offenses(surround("@var = #{o}")) 56 | end 57 | 58 | it "allows #{o} to be ||= to a class ivar" do 59 | expect_no_offenses(surround("@var ||= #{o}")) 60 | end 61 | end 62 | 63 | context 'when not directly in class / module' do 64 | describe 'top level code' do 65 | it_behaves_like 'immutable objects', '[1, 2, 3]' 66 | end 67 | 68 | describe 'inside a method' do 69 | let(:prefix) { "class Test\n def some_method" } 70 | let(:suffix) { " end\nend" } 71 | let(:indent) { ' ' } 72 | 73 | it_behaves_like 'immutable objects', '{ a: 1, b: 2 }' 74 | end 75 | 76 | describe 'inside a class method' do 77 | let(:prefix) { "class Test\n def self.some_method" } 78 | let(:suffix) { " end\nend" } 79 | let(:indent) { ' ' } 80 | 81 | it_behaves_like 'immutable objects', '%w(a b c)' 82 | end 83 | 84 | describe 'inside a class singleton method' do 85 | let(:prefix) do 86 | <<~RUBY 87 | class Test 88 | class << self 89 | def some_method 90 | RUBY 91 | end 92 | let(:suffix) do 93 | <<~RUBY 94 | end 95 | end 96 | end 97 | RUBY 98 | end 99 | let(:indent) { ' ' * 6 } 100 | 101 | it_behaves_like 'immutable objects', '%i{a b c}' 102 | end 103 | 104 | describe 'inside define_singleton_method' do 105 | let(:prefix) { "class Test\n define_singleton_method(:name) do" } 106 | let(:suffix) { " end\nend" } 107 | let(:indent) { ' ' } 108 | 109 | it_behaves_like 'immutable objects', '[1, 2]' 110 | end 111 | 112 | describe 'inside define_method' do 113 | let(:prefix) { "class Test\n define_method(:name) do" } 114 | let(:suffix) { " end\nend" } 115 | let(:indent) { ' ' } 116 | 117 | it_behaves_like 'immutable objects', '[1, 2]' 118 | end 119 | end 120 | 121 | context 'with Strict: false' do 122 | let(:cop_config) { { 'EnforcedStyle' => 'literals' } } 123 | 124 | %w[class module].each do |mod| 125 | context "when inside a #{mod}" do 126 | let(:prefix) { "#{mod} Test" } 127 | let(:suffix) { 'end' } 128 | let(:indent) { ' ' } 129 | 130 | it_behaves_like 'mutable objects', '[1, 2, 3]' 131 | it_behaves_like 'mutable objects', '%w(a b c)' 132 | it_behaves_like 'mutable objects', '{ a: 1, b: 2 }' 133 | it_behaves_like 'mutable objects', "'str'" 134 | it_behaves_like 'mutable objects', %("\#{30 + 12}ok") 135 | 136 | it_behaves_like 'immutable objects', '1' 137 | it_behaves_like 'immutable objects', '2.1' 138 | it_behaves_like 'immutable objects', ':sym' 139 | it_behaves_like 'immutable objects', 'CONST' 140 | it_behaves_like 'immutable objects', 'FOO + BAR' 141 | it_behaves_like 'immutable objects', 'FOO - BAR' 142 | it_behaves_like 'immutable objects', "'foo' + BAR" 143 | it_behaves_like 'immutable objects', "ENV['foo']" 144 | 145 | it_behaves_like 'immutable objects', '[1, 2].freeze' 146 | it_behaves_like 'immutable objects', 'Something.new' 147 | 148 | it 'registers no offense for class variable' do 149 | expect_no_offenses(surround('@@list = [1, 2]')) 150 | end 151 | 152 | describe 'inside an if statement' do 153 | let(:prefix) { "#{mod} Test\n if something" } 154 | let(:suffix) { " end\nend" } 155 | let(:indent) { ' ' } 156 | 157 | it_behaves_like 'mutable objects', '[1, 2, 3]' 158 | end 159 | 160 | context 'with splat expansion' do 161 | context 'with expansion of a range' do 162 | it 'registers an offense' do 163 | expect_offense(surround(<<~RUBY)) 164 | @var = *1..10 165 | ^^^^^^ #{msg} 166 | RUBY 167 | 168 | expect_correction(surround(<<~RUBY)) 169 | @var = (1..10).to_a.freeze 170 | RUBY 171 | end 172 | 173 | context 'with parentheses' do 174 | it 'registers an offense' do 175 | expect_offense(surround(<<~RUBY)) 176 | @var = *(1..10) 177 | ^^^^^^^^ #{msg} 178 | RUBY 179 | 180 | expect_correction(surround(<<~RUBY)) 181 | @var = (1..10).to_a.freeze 182 | RUBY 183 | end 184 | end 185 | end 186 | end 187 | 188 | context 'when assigning an array without brackets' do 189 | it 'adds brackets when auto-correcting' do 190 | expect_offense(surround(<<~RUBY)) 191 | @var = YYY, ZZZ 192 | ^^^^^^^^ #{msg} 193 | RUBY 194 | 195 | expect_correction(surround(<<~RUBY)) 196 | @var = [YYY, ZZZ].freeze 197 | RUBY 198 | end 199 | 200 | it 'does not add brackets to %w() arrays' do 201 | expect_offense(surround(<<~RUBY)) 202 | @var = %w(YYY ZZZ) 203 | ^^^^^^^^^^^ #{msg} 204 | RUBY 205 | 206 | expect_correction(surround(<<~RUBY)) 207 | @var = %w(YYY ZZZ).freeze 208 | RUBY 209 | end 210 | end 211 | 212 | context 'when assigning a range (irange) without parentheses' do 213 | it 'adds parentheses when auto-correcting' do 214 | expect_offense(surround(<<~RUBY)) 215 | @var = 1..99 216 | ^^^^^ #{msg} 217 | RUBY 218 | 219 | expect_correction(surround(<<~RUBY)) 220 | @var = (1..99).freeze 221 | RUBY 222 | end 223 | 224 | it 'does not add parenetheses to range enclosed in parentheses' do 225 | expect_offense(surround(<<~RUBY)) 226 | @var = (1..99) 227 | ^^^^^^^ #{msg} 228 | RUBY 229 | 230 | expect_correction(surround(<<~RUBY)) 231 | @var = (1..99).freeze 232 | RUBY 233 | end 234 | end 235 | 236 | context 'when assigning a range (erange) without parentheses' do 237 | it 'adds parentheses when auto-correcting' do 238 | expect_offense(surround(<<~RUBY)) 239 | @var = 1...99 240 | ^^^^^^ #{msg} 241 | RUBY 242 | 243 | expect_correction(surround(<<~RUBY)) 244 | @var = (1...99).freeze 245 | RUBY 246 | end 247 | 248 | it 'does not add parentheses to range enclosed in parentheses' do 249 | expect_offense(surround(<<~RUBY)) 250 | @var = (1...99) 251 | ^^^^^^^^ #{msg} 252 | RUBY 253 | 254 | expect_correction(surround(<<~RUBY)) 255 | @var = (1...99).freeze 256 | RUBY 257 | end 258 | end 259 | 260 | context 'with a frozen string literal' do 261 | # TODO: It is not yet decided when frozen string will be the default. 262 | # It has been abandoned for Ruby 3.0 but may default in the future. 263 | # So these tests are given a provisional value of 4.0. 264 | if defined?(RuboCop::TargetRuby) && 265 | RuboCop::TargetRuby.supported_versions.include?(4.0) 266 | context 'when the target ruby version >= 4.0' do 267 | let(:ruby_version) { 4.0 } 268 | 269 | context 'when the frozen_string_literal comment is missing' do 270 | it_behaves_like 'immutable objects', %("\#{a}") 271 | end 272 | 273 | context 'when the frozen_string_literal_comment is true' do 274 | let(:prefix) { "# frozen_string_literal: true\n#{super()}" } 275 | 276 | it_behaves_like 'immutable objects', %("\#{a}") 277 | end 278 | 279 | context 'when the frozen_string_literal_comment is false' do 280 | let(:prefix) { "# frozen_string_literal: false\n#{super()}" } 281 | 282 | it_behaves_like 'immutable objects', %("\#{a}") 283 | end 284 | end 285 | end 286 | 287 | context 'when the frozen_string_literal comment is missing' do 288 | it_behaves_like 'mutable objects', %("\#{a}") 289 | end 290 | 291 | context 'when the frozen_string_literal comment is true' do 292 | context 'with Ruby 2.7', unsupported_on: :prism do 293 | let(:prefix) { "# frozen_string_literal: true\n#{super()}" } 294 | 295 | it_behaves_like 'immutable objects', %("\#{a}") 296 | end 297 | 298 | context 'with Ruby 3', :ruby30 do 299 | it_behaves_like 'mutable objects', %("\#{a}") 300 | end 301 | end 302 | 303 | context 'when the frozen_string_literal comment is false' do 304 | let(:prefix) { "# frozen_string_literal: false\n#{super()}" } 305 | 306 | it_behaves_like 'mutable objects', %("\#{a}") 307 | end 308 | end 309 | 310 | context 'when assigning to multiple class ivars' do 311 | it 'registers an offense when first object is mutable' do 312 | expect_offense(surround(<<~RUBY)) 313 | @a, @b = [1], 1 314 | ^^^ #{msg} 315 | RUBY 316 | 317 | expect_correction(surround(<<~RUBY)) 318 | @a, @b = [1].freeze, 1 319 | RUBY 320 | end 321 | 322 | it 'registers an offense when middle object is mutable' do 323 | expect_offense(surround(<<~RUBY)) 324 | @a, @b, @c = [1, { a: 1 }, [3].freeze] 325 | ^^^^^^^^ #{msg} 326 | RUBY 327 | 328 | expect_correction(surround(<<~RUBY)) 329 | @a, @b, @c = [1, { a: 1 }.freeze, [3].freeze] 330 | RUBY 331 | end 332 | 333 | it 'registers an offense when last object is mutable' do 334 | expect_offense(surround(<<~RUBY)) 335 | @a, _, @c = 1, [2].freeze, 'foo' 336 | ^^^^^ #{msg} 337 | RUBY 338 | 339 | expect_correction(surround(<<~RUBY)) 340 | @a, _, @c = 1, [2].freeze, 'foo'.freeze 341 | RUBY 342 | end 343 | 344 | it 'registers an offense for multiple mutable objects' do 345 | expect_offense(surround(<<~RUBY)) 346 | @a, @b, @c = 'foo', [2], 3 347 | ^^^^^ #{msg} 348 | ^^^ #{msg} 349 | RUBY 350 | 351 | expect_correction(surround(<<~RUBY)) 352 | @a, @b, @c = 'foo'.freeze, [2].freeze, 3 353 | RUBY 354 | end 355 | 356 | it 'registers no offenses for multiple mutable objects without class' do 357 | expect_no_offenses(<<~RUBY) 358 | @a, @b, @c = 'foo', [2], 3 359 | RUBY 360 | end 361 | end 362 | 363 | it 'freezes a heredoc' do 364 | expect_offense(surround(<<~RUBY)) 365 | @var = <<~HERE 366 | ^^^^^^^ #{msg} 367 | content 368 | HERE 369 | RUBY 370 | 371 | expect_correction(surround(<<~RUBY)) 372 | @var = <<~HERE.freeze 373 | content 374 | HERE 375 | RUBY 376 | end 377 | end 378 | end 379 | end 380 | 381 | context 'with Strict: true' do 382 | let(:cop_config) { { 'EnforcedStyle' => 'strict' } } 383 | 384 | %w[class module].each do |mod| 385 | context "when inside a #{mod}" do 386 | let(:prefix) { "#{mod} Test" } 387 | let(:suffix) { 'end' } 388 | let(:indent) { ' ' } 389 | 390 | it_behaves_like 'mutable objects', '[1, 2, 3]' 391 | it_behaves_like 'mutable objects', '%w(a b c)' 392 | it_behaves_like 'mutable objects', '{ a: 1, b: 2 }' 393 | it_behaves_like 'mutable objects', "'str'" 394 | it_behaves_like 'mutable objects', %("\#{30 + 12}ok") 395 | it_behaves_like 'mutable objects', 'Something.new' 396 | 397 | it_behaves_like 'immutable objects', '1' 398 | it_behaves_like 'immutable objects', '2.1' 399 | it_behaves_like 'immutable objects', ':sym' 400 | it_behaves_like 'immutable objects', 'CONST' 401 | it_behaves_like 'immutable objects', '::CONST' 402 | it_behaves_like 'immutable objects', 'Namespace::CONST' 403 | it_behaves_like 'immutable objects', '::Namespace::CONST' 404 | it_behaves_like 'immutable objects', 'Struct.new' 405 | it_behaves_like 'immutable objects', '::Struct.new' 406 | it_behaves_like 'immutable objects', 'Struct.new(:a, :b)' 407 | it_behaves_like 'immutable objects', '::Struct.new(:a, :b)' 408 | it_behaves_like 'immutable objects', <<~RUBY 409 | Struct.new(:node) do 410 | def assignment? 411 | true 412 | end 413 | end 414 | RUBY 415 | it_behaves_like 'immutable objects', <<~RUBY 416 | ::Struct.new(:node) do 417 | def assignment? 418 | true 419 | end 420 | end 421 | RUBY 422 | 423 | it_behaves_like 'immutable objects', '[1, 2].freeze' 424 | it_behaves_like 'immutable objects', 'Something.new.freeze' 425 | it_behaves_like 'immutable objects', '::Something.new.freeze' 426 | 427 | context 'with thread-safe data structure' do 428 | it_behaves_like 'immutable objects', 'Queue.new' 429 | it_behaves_like 'immutable objects', '::Queue.new' 430 | it_behaves_like 'immutable objects', 'ThreadSafe::Array.new' 431 | it_behaves_like 'immutable objects', '::ThreadSafe::Hash.new' 432 | it_behaves_like 'immutable objects', 'ThreadSafe::Hash.new { false }' 433 | it_behaves_like 'immutable objects', 'Concurrent::Array.new' 434 | it_behaves_like 'immutable objects', 'Concurrent::Hash.new' 435 | it_behaves_like 'immutable objects', '::Concurrent::Map.new' 436 | it_behaves_like 'immutable objects', <<~RUBY 437 | Concurrent::Map.new(initial_capacity: 4) 438 | RUBY 439 | it_behaves_like 'immutable objects', <<~RUBY 440 | Concurrent::Map.new do |h, key| 441 | h.fetch_or_store(key, Concurrent::Map.new) 442 | end 443 | RUBY 444 | it_behaves_like 'immutable objects', 445 | 'Concurrent::ContinuationQueue.new' 446 | it_behaves_like 'immutable objects', 447 | 'Concurrent::ThreadPoolExecutor.new(options)' 448 | it_behaves_like 'immutable objects', 449 | 'Concurrent::AtomicBoolean.new(true)' 450 | it_behaves_like 'immutable objects', 451 | 'Concurrent::ThreadSafe::Util::Adder.new' 452 | 453 | it_behaves_like 'mutable objects', '[Queue.new]' 454 | it_behaves_like 'mutable objects', '[ThreadSafe::Hash.new { false }]' 455 | it_behaves_like 'mutable objects', 456 | '[Concurrent::ThreadSafe::Util::Adder.new]' 457 | end 458 | 459 | it 'registers no offense for class variable' do 460 | expect_no_offenses(surround('@@list = [1, 2]')) 461 | end 462 | 463 | describe 'inside an if statement' do 464 | let(:prefix) { "#{mod} Test\n if something" } 465 | let(:suffix) { " end\nend" } 466 | let(:indent) { ' ' } 467 | 468 | it_behaves_like 'mutable objects', '[1, 2, 3]' 469 | end 470 | 471 | context 'with splat expansion' do 472 | context 'with expansion of a range' do 473 | it 'registers an offense' do 474 | expect_offense(surround(<<~RUBY)) 475 | @var = *1..10 476 | ^^^^^^ #{msg} 477 | RUBY 478 | 479 | expect_correction(surround(<<~RUBY)) 480 | @var = (1..10).to_a.freeze 481 | RUBY 482 | end 483 | 484 | context 'with parentheses' do 485 | it 'registers an offense' do 486 | expect_offense(surround(<<~RUBY)) 487 | @var = *(1..10) 488 | ^^^^^^^^ #{msg} 489 | RUBY 490 | 491 | expect_correction(surround(<<~RUBY)) 492 | @var = (1..10).to_a.freeze 493 | RUBY 494 | end 495 | end 496 | end 497 | end 498 | 499 | context 'when assigning with an operator' do 500 | shared_examples 'operator methods' do |o| 501 | it 'registers an offense' do 502 | expect_offense(surround(<<~RUBY), o: o) 503 | @var = FOO %{o} BAR 504 | ^^^^^{o}^^^^ #{msg} 505 | RUBY 506 | 507 | expect_correction(surround(<<~RUBY)) 508 | @var = (FOO #{o} BAR).freeze 509 | RUBY 510 | end 511 | end 512 | 513 | it_behaves_like 'operator methods', '+' 514 | it_behaves_like 'operator methods', '-' 515 | it_behaves_like 'operator methods', '*' 516 | it_behaves_like 'operator methods', '/' 517 | it_behaves_like 'operator methods', '%' 518 | it_behaves_like 'operator methods', '**' 519 | end 520 | 521 | context 'when assigning with multiple operator calls' do 522 | it 'registers an offense' do 523 | expect_offense(surround(<<~RUBY)) 524 | @a = [1].freeze 525 | @b = [2].freeze 526 | @c = [3].freeze 527 | @var = @a + @b + @c 528 | ^^^^^^^^^^^^ #{msg} 529 | RUBY 530 | 531 | expect_correction(surround(<<~RUBY)) 532 | @a = [1].freeze 533 | @b = [2].freeze 534 | @c = [3].freeze 535 | @var = (@a + @b + @c).freeze 536 | RUBY 537 | end 538 | end 539 | 540 | context 'with methods and operators that produce frozen objects' do 541 | it_behaves_like 'immutable objects', "ENV['foo'] || 'bar'" 542 | it_behaves_like 'immutable objects', 'FOO + 2' 543 | it_behaves_like 'immutable objects', '1 + 2' 544 | it_behaves_like 'immutable objects', 'FOO + 2.1' 545 | it_behaves_like 'immutable objects', '1.2 + 3.4' 546 | it_behaves_like 'immutable objects', 'FOO == BAR' 547 | 548 | describe 'checking fixed size' do 549 | it_behaves_like 'immutable objects', "'foo'.count" 550 | it_behaves_like 'immutable objects', "'foo'.count('f')" 551 | it_behaves_like 'immutable objects', '[1, 2, 3].count { |n| n > 2 }' 552 | it_behaves_like 'immutable objects', '[1, 2].count(2) { |n| n > 2 }' 553 | it_behaves_like 'immutable objects', "'foo'.length" 554 | it_behaves_like 'immutable objects', "'foo'.size" 555 | end 556 | end 557 | 558 | context 'with operators that produce unfrozen objects' do 559 | it 'registers an offense when operating on a constant and a string' do 560 | expect_offense(surround(<<~RUBY)) 561 | @var = FOO + 'bar' 562 | ^^^^^^^^^^^ #{msg} 563 | RUBY 564 | 565 | expect_correction(surround(<<~RUBY)) 566 | @var = (FOO + 'bar').freeze 567 | RUBY 568 | end 569 | 570 | it 'registers an offense when operating on multiple strings' do 571 | expect_offense(surround(<<~RUBY)) 572 | @var = 'foo' + 'bar' + 'baz' 573 | ^^^^^^^^^^^^^^^^^^^^^ #{msg} 574 | RUBY 575 | 576 | expect_correction(surround(<<~RUBY)) 577 | @var = ('foo' + 'bar' + 'baz').freeze 578 | RUBY 579 | end 580 | end 581 | 582 | context 'when assigning an array without brackets' do 583 | it 'adds brackets when auto-correcting' do 584 | expect_offense(surround(<<~RUBY)) 585 | @var = @a, @b 586 | ^^^^^^ #{msg} 587 | RUBY 588 | 589 | expect_correction(surround(<<~RUBY)) 590 | @var = [@a, @b].freeze 591 | RUBY 592 | end 593 | 594 | it 'does not add brackets to %w() arrays' do 595 | expect_offense(surround(<<~RUBY)) 596 | @var = %w(YYY ZZZ) 597 | ^^^^^^^^^^^ #{msg} 598 | RUBY 599 | 600 | expect_correction(surround(<<~RUBY)) 601 | @var = %w(YYY ZZZ).freeze 602 | RUBY 603 | end 604 | end 605 | 606 | it 'freezes a heredoc' do 607 | expect_offense(surround(<<~RUBY)) 608 | @var = <<~HERE 609 | ^^^^^^^ #{msg} 610 | content 611 | HERE 612 | RUBY 613 | 614 | expect_correction(surround(<<~RUBY)) 615 | @var = <<~HERE.freeze 616 | content 617 | HERE 618 | RUBY 619 | end 620 | 621 | context 'with a frozen string literal' do 622 | context 'when the frozen_string_literal comment is missing' do 623 | it_behaves_like 'mutable objects', %("\#{a}") 624 | end 625 | 626 | context 'when the frozen_string_literal comment is true' do 627 | context 'with Ruby 2.7', unsupported_on: :prism do 628 | let(:prefix) { "# frozen_string_literal: true\n#{super()}" } 629 | 630 | it_behaves_like 'immutable objects', %("\#{a}") 631 | end 632 | 633 | context 'with Ruby 3', :ruby30 do 634 | let(:prefix) { "# frozen_string_literal: true\n#{super()}" } 635 | 636 | it_behaves_like 'mutable objects', %("\#{a}") 637 | end 638 | end 639 | 640 | context 'when the frozen_string_literal comment is false' do 641 | let(:prefix) { "# frozen_string_literal: false\n#{super()}" } 642 | 643 | it_behaves_like 'mutable objects', %("\#{a}") 644 | end 645 | end 646 | 647 | context 'when assigning to multiple class ivars' do 648 | it 'registers an offense when first object is mutable' do 649 | expect_offense(surround(<<~RUBY)) 650 | @a, @b = [1], 1 651 | ^^^ #{msg} 652 | RUBY 653 | 654 | expect_correction(surround(<<~RUBY)) 655 | @a, @b = [1].freeze, 1 656 | RUBY 657 | end 658 | 659 | it 'registers an offense when middle object is mutable' do 660 | expect_offense(surround(<<~RUBY)) 661 | @a, @b, @c = [1, { a: 1 }, [3].freeze] 662 | ^^^^^^^^ #{msg} 663 | RUBY 664 | 665 | expect_correction(surround(<<~RUBY)) 666 | @a, @b, @c = [1, { a: 1 }.freeze, [3].freeze] 667 | RUBY 668 | end 669 | 670 | it 'registers an offense when last object is mutable' do 671 | expect_offense(surround(<<~RUBY)) 672 | @a, _, @c = 1, [2].freeze, 'foo' 673 | ^^^^^ #{msg} 674 | RUBY 675 | 676 | expect_correction(surround(<<~RUBY)) 677 | @a, _, @c = 1, [2].freeze, 'foo'.freeze 678 | RUBY 679 | end 680 | 681 | it 'registers an offense for multiple mutable objects' do 682 | expect_offense(surround(<<~RUBY)) 683 | @a, @b, @c = 'foo', [2], 3 684 | ^^^^^ #{msg} 685 | ^^^ #{msg} 686 | RUBY 687 | 688 | expect_correction(surround(<<~RUBY)) 689 | @a, @b, @c = 'foo'.freeze, [2].freeze, 3 690 | RUBY 691 | end 692 | 693 | it 'registers no offenses for single assignment' do 694 | expect_no_offenses(surround(<<~RUBY)) 695 | @a, @b = 1 696 | RUBY 697 | end 698 | end 699 | end 700 | end 701 | end 702 | end 703 | # rubocop:enable RSpec/ExampleLength,RSpec/MultipleMemoizedHelpers,RSpec/NestedGroups 704 | -------------------------------------------------------------------------------- /spec/rubocop/cop/thread_safety/new_thread_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RuboCop::Cop::ThreadSafety::NewThread, :config do 4 | %w[new fork start].each do |method_name| 5 | it "registers an offense for `#{Thread}.#{method_name}`" do 6 | expect_offense(<<~RUBY, method_name: method_name) 7 | Thread.%{method_name} { do_work } 8 | ^^^^^^^^{method_name} Avoid starting new threads. 9 | RUBY 10 | end 11 | 12 | it "registers an offense for `#{Thread}.#{method_name}` with positional arguments" do 13 | expect_offense(<<~RUBY, method_name: method_name) 14 | Thread.%{method_name}(1) { do_work } 15 | ^^^^^^^^{method_name}^^^ Avoid starting new threads. 16 | RUBY 17 | end 18 | 19 | it "registers an offense for `#{Thread}.#{method_name}` with keyword arguments" do 20 | expect_offense(<<~RUBY, method_name: method_name) 21 | Thread.%{method_name}(a: 42) { do_work } 22 | ^^^^^^^^{method_name}^^^^^^^ Avoid starting new threads. 23 | RUBY 24 | end 25 | 26 | it "registers an offense for `#{Thread}.#{method_name}` with fully qualified constant name" do 27 | expect_offense(<<~RUBY, method_name: method_name) 28 | ::Thread.%{method_name}(a: 42) { do_work } 29 | ^^^^^^^^^^{method_name}^^^^^^^ Avoid starting new threads. 30 | RUBY 31 | end 32 | 33 | it "registers an offense for `#{Thread}.#{method_name}` with safe navigation" do 34 | expect_offense(<<~RUBY, method_name: method_name) 35 | Thread&.%{method_name}(a: 42) { do_work } 36 | ^^^^^^^^^{method_name}^^^^^^^ Avoid starting new threads. 37 | RUBY 38 | end 39 | 40 | it "registers an offense for `#{Thread}.#{method_name}` with block argument" do 41 | expect_offense(<<~RUBY, method_name: method_name) 42 | Thread&.%{method_name}(&block) 43 | ^^^^^^^^^{method_name}^^^^^^^^ Avoid starting new threads. 44 | RUBY 45 | end 46 | 47 | it "registers an offense for `#{Thread}.#{method_name}` with block and other arguments" do 48 | expect_offense(<<~RUBY, method_name: method_name) 49 | Thread&.%{method_name}(1, a: 42, &block) 50 | ^^^^^^^^^{method_name}^^^^^^^^^^^^^^^^^^ Avoid starting new threads. 51 | RUBY 52 | end 53 | 54 | it 'does not register an offense for unrelated receiver' do 55 | expect_no_offenses("Other.#{method_name} { do_work }") 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/rubocop/cop/thread_safety/rack_middleware_instance_variable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RuboCop::Cop::ThreadSafety::RackMiddlewareInstanceVariable, :config do 4 | let(:msg) { 'Avoid instance variables in Rack middleware.' } 5 | 6 | context 'with unrelated source' do 7 | it { expect_no_offenses '' } 8 | 9 | specify do 10 | expect_no_offenses(<<~RUBY) 11 | class SomeClass 12 | def initialize(user) 13 | @user = user 14 | end 15 | end 16 | RUBY 17 | end 18 | 19 | specify do 20 | expect_no_offenses(<<~RUBY) 21 | class SomeClass 22 | def initialize(user, context) 23 | @user = user 24 | @context = context 25 | end 26 | end 27 | RUBY 28 | end 29 | 30 | specify do 31 | expect_no_offenses(<<~RUBY) 32 | class SomeClass 33 | def call(env) 34 | @env = env 35 | end 36 | end 37 | RUBY 38 | end 39 | 40 | specify do 41 | expect_no_offenses(<<~RUBY) 42 | class SomeClass 43 | def initialize(user, context) 44 | @user = user 45 | @context = context 46 | end 47 | 48 | def call 49 | [@user, @context] 50 | end 51 | end 52 | RUBY 53 | end 54 | 55 | specify do 56 | expect_no_offenses(<<~RUBY) 57 | class SomeClass 58 | def initialize(app) 59 | @user = User.new 60 | end 61 | 62 | def call(env) 63 | @x = TOPLEVEL_BINDING 64 | end 65 | end 66 | RUBY 67 | end 68 | 69 | specify do 70 | expect_no_offenses(<<~RUBY) 71 | class SomeClass 72 | def initialize(app) 73 | @app = app 74 | @user = User.new 75 | end 76 | 77 | def call(env, user) 78 | @x = TOPLEVEL_BINDING 79 | end 80 | end 81 | RUBY 82 | end 83 | end 84 | 85 | it 'does not register an offense' do 86 | expect_no_offenses(<<~RUBY) 87 | class TestMiddleware 88 | def initialize(app) 89 | @app = app 90 | end 91 | 92 | def call(env) 93 | @app.call(env) 94 | end 95 | end 96 | RUBY 97 | end 98 | 99 | it 'registers an offense' do 100 | expect_offense(<<~RUBY) 101 | class TestMiddleware 102 | def initialize(app) 103 | @app = app 104 | @foo = 1 105 | ^^^^^^^^ #{msg} 106 | end 107 | 108 | def call(env) 109 | @app.call(env) 110 | p @foo 111 | ^^^^ #{msg} 112 | end 113 | end 114 | RUBY 115 | 116 | expect_offense(<<~RUBY) 117 | class TestMiddleware 118 | def initialize(app) 119 | @app = app 120 | @counter = 0 121 | ^^^^^^^^^^^^ #{msg} 122 | end 123 | 124 | def call(env) 125 | @app.call(env) 126 | ensure 127 | @counter += 1 128 | ^^^^^^^^ #{msg} 129 | end 130 | end 131 | RUBY 132 | end 133 | 134 | it 'registers an offense with thread-safe wrappers' do 135 | expect_offense(<<~RUBY) 136 | class TestMiddleware 137 | def initialize(app) 138 | @app = app 139 | @counter = Concurrent::AtomicReference.new(0) 140 | @unsafe_counter = 0 141 | ^^^^^^^^^^^^^^^^^^^ Avoid instance variables in Rack middleware. 142 | end 143 | 144 | def call(env) 145 | @app.call(env) 146 | ensure 147 | @unsafe_counter += 1 148 | ^^^^^^^^^^^^^^^ Avoid instance variables in Rack middleware. 149 | @counter.update { |ref| ref + 1 } 150 | end 151 | end 152 | RUBY 153 | end 154 | 155 | it 'registers an offense with mismatched local and instance variables' do 156 | expect_offense(<<~RUBY) 157 | class TestMiddleware 158 | def initialize(app) 159 | @foo = fsa 160 | ^^^^^^^^^^ #{msg} 161 | @a = app 162 | end 163 | 164 | def call(env) 165 | @a.call(env) 166 | end 167 | end 168 | RUBY 169 | end 170 | 171 | it 'registers an offense for nested middleware' do 172 | expect_offense(<<~RUBY) 173 | module MyMiddlewares 174 | class TestMiddleware 175 | def initialize(app) 176 | @app = app 177 | @foo = 1 178 | ^^^^^^^^ #{msg} 179 | end 180 | 181 | def call(env) 182 | @app.call(env) 183 | end 184 | end 185 | end 186 | RUBY 187 | end 188 | 189 | it 'registers an offense for multiple middlewares' do 190 | expect_offense(<<~RUBY) 191 | module MyMiddlewares 192 | class TestMiddleware 193 | def initialize(app) 194 | @app = app 195 | @foo = 1 196 | ^^^^^^^^ #{msg} 197 | end 198 | 199 | def call(env) 200 | @app.call(env) 201 | end 202 | end 203 | 204 | class TestMiddleware2 205 | def initialize(app) 206 | @app = app 207 | @foo = 1 208 | ^^^^^^^^ #{msg} 209 | end 210 | 211 | def call(env) 212 | @app.call(env) 213 | end 214 | end 215 | end 216 | RUBY 217 | end 218 | 219 | it 'registers an offense for extra methods' do 220 | expect_offense(<<~RUBY) 221 | class TestMiddleware 222 | def initialize(app) 223 | @app = app 224 | end 225 | 226 | def call(env) 227 | @app.call(env) 228 | @a = 1 229 | ^^^^^^ #{msg} 230 | end 231 | 232 | def foo 233 | @a = 1 234 | ^^^^^^ #{msg} 235 | end 236 | end 237 | RUBY 238 | 239 | expect_offense(<<~RUBY) 240 | class TestMiddleware 241 | def foo 242 | @a = 1 243 | ^^^^^^ #{msg} 244 | end 245 | 246 | def initialize(app) 247 | @app = app 248 | end 249 | 250 | def call(env) 251 | @app.call(env) 252 | end 253 | end 254 | RUBY 255 | 256 | expect_offense(<<~RUBY) 257 | class TestMiddleware 258 | def foo 259 | @a = 1 260 | ^^^^^^ #{msg} 261 | end 262 | 263 | def initialize(app) 264 | @app = app 265 | end 266 | 267 | def call(env) 268 | @app.call(env) 269 | end 270 | 271 | def bar 272 | @b = 1 273 | ^^^^^^ #{msg} 274 | end 275 | end 276 | RUBY 277 | end 278 | 279 | it 'registers an offense with `call` before constructor definition' do 280 | expect_offense(<<~RUBY) 281 | class TestMiddleware 282 | def call(env) 283 | @app.call(env) 284 | end 285 | 286 | def initialize(app) 287 | @app = app 288 | @foo = 1 289 | ^^^^^^^^ #{msg} 290 | end 291 | end 292 | RUBY 293 | end 294 | 295 | context 'with `instance_variable_set` and `instance_variable_get` methods' do 296 | it 'registers an offense' do 297 | expect_offense(<<~RUBY) 298 | class TestMiddleware 299 | def initialize(app) 300 | @app = app 301 | instance_variable_set(:counter, 1) 302 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 303 | end 304 | 305 | def call(env) 306 | @app.call(env) 307 | instance_variable_get("@counter") 308 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 309 | end 310 | end 311 | RUBY 312 | end 313 | 314 | it 'registers an offense with safe navigation' do 315 | expect_offense(<<~RUBY) 316 | class TestMiddleware 317 | def initialize(app) 318 | @app = app 319 | foo = SomeClass.new 320 | instance_variable_set(:counter, 1) 321 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 322 | end 323 | 324 | def call(env) 325 | @app.call(env) 326 | instance_variable_get("@counter") 327 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} 328 | end 329 | end 330 | RUBY 331 | end 332 | 333 | it 'registers no offenses' do 334 | expect_no_offenses(<<~RUBY) 335 | class TestMiddleware 336 | def initialize(app) 337 | @app = app 338 | instance_variable_set 339 | end 340 | 341 | def call(env) 342 | @app.call(env) 343 | instance_variable_get 344 | end 345 | end 346 | RUBY 347 | 348 | expect_no_offenses(<<~RUBY) 349 | class TestMiddleware 350 | def initialize(app) 351 | @app = app 352 | instance_variable_set(1) 353 | end 354 | 355 | def call(env) 356 | @app.call(env) 357 | instance_variable_get({}) 358 | end 359 | end 360 | RUBY 361 | end 362 | 363 | context 'with non-empty `AllowedIdentifiers` config' do 364 | let(:cop_config) do 365 | { 'AllowedIdentifiers' => ['options'] } 366 | end 367 | 368 | it 'registers no offenses' do 369 | expect_no_offenses(<<~RUBY) 370 | class TestMiddleware 371 | def initialize(app) 372 | @app = app 373 | instance_variable_set(:@options, {}) 374 | end 375 | 376 | def call(env) 377 | @app.call(env) 378 | end 379 | end 380 | RUBY 381 | end 382 | end 383 | end 384 | 385 | context 'with non-empty `AllowedIdentifiers` config' do 386 | let(:cop_config) do 387 | { 'AllowedIdentifiers' => ['options'] } 388 | end 389 | 390 | it 'registers an offense' do 391 | expect_offense(<<~RUBY) 392 | class TestMiddleware 393 | def call(env) 394 | @app.call(env) 395 | end 396 | 397 | def initialize(app) 398 | @app = app 399 | @some_var = 2 400 | ^^^^^^^^^^^^^ #{msg} 401 | @options = 1 402 | end 403 | end 404 | RUBY 405 | end 406 | end 407 | 408 | context 'with middleware with provided options' do 409 | it 'registers an offense' do 410 | expect_offense(<<~RUBY) 411 | class TestMiddleware 412 | def initialize(app, options) 413 | @app = app 414 | @options = options 415 | ^^^^^^^^^^^^^^^^^^ #{msg} 416 | end 417 | 418 | def call(env) 419 | if options[:noop] 420 | [200, {}, []] 421 | else 422 | @app.call(env) 423 | end 424 | end 425 | end 426 | RUBY 427 | end 428 | end 429 | end 430 | -------------------------------------------------------------------------------- /spec/rubocop/thread_safety_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RuboCop::ThreadSafety do 4 | it 'has a version number' do 5 | expect(RuboCop::ThreadSafety::Version::STRING).to match(/\d+\.\d+.\d+/) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | 5 | SimpleCov.start do 6 | add_filter '/spec' 7 | 8 | enable_coverage :branch 9 | end 10 | 11 | begin 12 | require 'pry' 13 | rescue LoadError 14 | # Pry isn't installed in CI. 15 | end 16 | 17 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 18 | require 'rubocop-thread_safety' 19 | require 'rubocop-ast' 20 | 21 | require 'rubocop/rspec/support' 22 | 23 | RSpec.configure do |config| 24 | config.expect_with :rspec do |expectations| 25 | expectations.syntax = :expect 26 | end 27 | 28 | config.mock_with :rspec do |mocks| 29 | mocks.syntax = :expect 30 | mocks.verify_partial_doubles = true 31 | end 32 | 33 | if ENV.key? 'CI' 34 | config.before(:example, :focus) { raise 'Should not commit focused specs' } 35 | else 36 | config.filter_run focus: true 37 | config.run_all_when_everything_filtered = true 38 | config.fail_fast = ENV.key? 'RSPEC_FAIL_FAST' 39 | end 40 | 41 | config.around :each, :with_legacy_lambda_node do |example| 42 | initial_value = RuboCop::AST::Builder.emit_lambda 43 | RuboCop::AST::Builder.emit_lambda = true 44 | 45 | example.call 46 | ensure 47 | RuboCop::AST::Builder.emit_lambda = initial_value 48 | end 49 | 50 | config.filter_run_excluding unsupported_on: :prism if ENV['PARSER_ENGINE'] == 'parser_prism' 51 | 52 | config.disable_monkey_patching! 53 | 54 | config.order = :random 55 | 56 | Kernel.srand config.seed 57 | 58 | config.include_context 'ruby 2.7', :ruby27 59 | config.include_context 'ruby 3.0', :ruby30 60 | config.include_context 'ruby 3.1', :ruby31 61 | config.include_context 'ruby 3.2', :ruby32 62 | config.include_context 'ruby 3.3', :ruby33 63 | config.include_context 'ruby 3.4', :ruby34 64 | end 65 | -------------------------------------------------------------------------------- /tasks/cops_documentation.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubocop' 4 | require 'rubocop-thread_safety' 5 | require 'rubocop/cops_documentation_generator' 6 | require 'yard' 7 | 8 | YARD::Rake::YardocTask.new(:yard_for_generate_documentation) do |task| 9 | task.files = ['lib/rubocop/cop/**/*.rb'] 10 | task.options = ['--no-output'] 11 | end 12 | 13 | desc 'Generate docs of all cops departments' 14 | task generate_cops_documentation: :yard_for_generate_documentation do 15 | RuboCop::ConfigLoader.inject_defaults!("#{__dir__}/../config/default.yml") 16 | 17 | deps = ['ThreadSafety'] 18 | CopsDocumentationGenerator.new(departments: deps).call 19 | end 20 | 21 | desc 'Syntax check for the documentation comments' 22 | task documentation_syntax_check: :yard_for_generate_documentation do 23 | require 'parser/ruby31' 24 | 25 | ok = true 26 | YARD::Registry.load! 27 | cops = RuboCop::Cop::Registry.global 28 | cops.each do |cop| 29 | examples = YARD::Registry.all(:class).find do |code_object| 30 | next unless RuboCop::Cop::Badge.for(code_object.to_s) == cop.badge 31 | 32 | break code_object.tags('example') 33 | end 34 | 35 | examples.to_a.each do |example| 36 | buffer = Parser::Source::Buffer.new('', 1) 37 | buffer.source = example.text 38 | parser = Parser::Ruby31.new(RuboCop::AST::Builder.new) 39 | parser.diagnostics.all_errors_are_fatal = true 40 | parser.parse(buffer) 41 | rescue Parser::SyntaxError => e 42 | path = example.object.file 43 | puts "#{path}: Syntax Error in an example. #{e}" 44 | ok = false 45 | end 46 | end 47 | abort unless ok 48 | end 49 | --------------------------------------------------------------------------------