forked from covermymeds/rubocop-thread_safety
-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #52 from viralpraxis/implement-rack-middleware-ins…
…tance-variable-cop Implement `ThreadSafety/RackMiddlewareInstanceVariable` cop
- Loading branch information
Showing
10 changed files
with
669 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# frozen_string_literal: true | ||
|
||
module RuboCop | ||
module Cop | ||
# Common functionality for checking if a well-known operation | ||
# produces an object with thread-safe semantics. | ||
module OperationWithThreadsafeResult | ||
extend NodePattern::Macros | ||
|
||
# @!method operation_produces_threadsafe_object?(node) | ||
def_node_matcher :operation_produces_threadsafe_object?, <<~PATTERN | ||
{ | ||
(send (const {nil? cbase} :Queue) :new ...) | ||
(send | ||
(const (const {nil? cbase} :ThreadSafe) {:Hash :Array}) | ||
:new ...) | ||
(block | ||
(send | ||
(const (const {nil? cbase} :ThreadSafe) {:Hash :Array}) | ||
:new ...) | ||
...) | ||
(send (const (const {nil? cbase} :Concurrent) _) :new ...) | ||
(block | ||
(send (const (const {nil? cbase} :Concurrent) _) :new ...) | ||
...) | ||
(send (const (const (const {nil? cbase} :Concurrent) _) _) :new ...) | ||
(block | ||
(send | ||
(const (const (const {nil? cbase} :Concurrent) _) _) | ||
:new ...) | ||
...) | ||
(send | ||
(const (const (const (const {nil? cbase} :Concurrent) _) _) _) | ||
:new ...) | ||
(block | ||
(send | ||
(const (const (const (const {nil? cbase} :Concurrent) _) _) _) | ||
:new ...) | ||
...) | ||
} | ||
PATTERN | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
lib/rubocop/cop/thread_safety/rack_middleware_instance_variable.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
# frozen_string_literal: true | ||
|
||
module RuboCop | ||
module Cop | ||
module ThreadSafety | ||
# Avoid instance variables in rack middleware. | ||
# | ||
# Middlewares are initialized once, meaning any instance variables are shared between executor threads. | ||
# To avoid potential race conditions, it's recommended to design middlewares to be stateless | ||
# or to implement proper synchronization mechanisms. | ||
# | ||
# @example | ||
# # bad | ||
# class CounterMiddleware | ||
# def initialize(app) | ||
# @app = app | ||
# @counter = 0 | ||
# end | ||
# | ||
# def call(env) | ||
# app.call(env) | ||
# ensure | ||
# @counter += 1 | ||
# end | ||
# end | ||
# | ||
# # good | ||
# class CounterMiddleware | ||
# def initialize(app) | ||
# @app = app | ||
# @counter = Concurrent::AtomicReference.new(0) | ||
# end | ||
# | ||
# def call(env) | ||
# app.call(env) | ||
# ensure | ||
# @counter.update { |ref| ref + 1 } | ||
# end | ||
# end | ||
# | ||
# class IdentityMiddleware | ||
# def initialize(app) | ||
# @app = app | ||
# end | ||
# | ||
# def call(env) | ||
# app.call(env) | ||
# end | ||
# end | ||
class RackMiddlewareInstanceVariable < Base | ||
include AllowedIdentifiers | ||
include OperationWithThreadsafeResult | ||
|
||
MSG = 'Avoid instance variables in Rack middleware.' | ||
|
||
RESTRICT_ON_SEND = %i[instance_variable_get instance_variable_set].freeze | ||
|
||
# @!method rack_middleware_like_class?(node) | ||
def_node_matcher :rack_middleware_like_class?, <<~MATCHER | ||
(class (const nil? _) nil? (begin <(def :initialize (args (arg _)+) ...) (def :call (args (arg _)) ...) ...>)) | ||
MATCHER | ||
|
||
# @!method app_variable(node) | ||
def_node_search :app_variable, <<~MATCHER | ||
(def :initialize (args (arg $_) ...) `(ivasgn $_ (lvar $_))) | ||
MATCHER | ||
|
||
def on_class(node) | ||
return unless rack_middleware_like_class?(node) | ||
|
||
constructor_method = find_constructor_method(node) | ||
return unless (application_variable = extract_application_variable_from_contructor_method(constructor_method)) | ||
|
||
safe_variables = extract_safe_variables_from_constructor_method(constructor_method) | ||
|
||
node.each_node(:def) do |def_node| | ||
def_node.each_node(:ivasgn, :ivar) do |ivar_node| | ||
variable, = ivar_node.to_a | ||
if variable == application_variable || safe_variables.include?(variable) || allowed_identifier?(variable) | ||
next | ||
end | ||
|
||
add_offense ivar_node | ||
end | ||
end | ||
end | ||
|
||
def on_send(node) | ||
argument = node.first_argument | ||
|
||
return unless argument&.sym_type? || argument&.str_type? | ||
return if allowed_identifier?(argument.value) | ||
|
||
add_offense node | ||
end | ||
|
||
private | ||
|
||
def find_constructor_method(class_node) | ||
class_node | ||
.each_node(:def) | ||
.find { |node| node.method?(:initialize) && node.arguments.size >= 1 } | ||
end | ||
|
||
def extract_application_variable_from_contructor_method(constructor_method) | ||
constructor_method | ||
.then { |node| app_variable(node) } | ||
.then { |variables| variables.first[1] if variables.first } | ||
end | ||
|
||
def extract_safe_variables_from_constructor_method(constructor_method) | ||
constructor_method | ||
.each_node(:ivasgn) | ||
.select { |ivasgn_node| operation_produces_threadsafe_object?(ivasgn_node.to_a[1]) } | ||
.map { _1.to_a[0] } | ||
end | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.