Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new Lint/DeprecatedOpenSSLConstant cop #7950

Merged
merged 1 commit into from May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@
### New features

* [#7735](https://github.com/rubocop-hq/rubocop/issues/7735): `NodePattern` and `AST` classes have been moved to the [`rubocop-ast` gem](https://github.com/rubocop-hq/rubocop-ast). ([@marcandre][])
* [#7950](https://github.com/rubocop-hq/rubocop/pull/7950): Add new `Lint/DeprecatedOpenSSLConstant` cop. ([@bdewater][])

### Bug fixes

Expand Down
5 changes: 5 additions & 0 deletions config/default.yml
Expand Up @@ -1370,6 +1370,11 @@ Lint/DeprecatedClassMethods:
Enabled: true
VersionAdded: '0.19'

Lint/DeprecatedOpenSSLConstant:
Description: "Don't use algorithm constants for `OpenSSL::Cipher` and `OpenSSL::Digest`."
Enabled: pending
VersionAdded: '0.84'

Lint/DisjunctiveAssignmentInConstructor:
Description: 'In constructor, plain assignment is preferred over disjunctive.'
Enabled: true
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Expand Up @@ -237,6 +237,7 @@
require_relative 'rubocop/cop/lint/circular_argument_reference'
require_relative 'rubocop/cop/lint/debugger'
require_relative 'rubocop/cop/lint/deprecated_class_methods'
require_relative 'rubocop/cop/lint/deprecated_open_ssl_constant'
require_relative 'rubocop/cop/lint/disjunctive_assignment_in_constructor'
require_relative 'rubocop/cop/lint/duplicate_case_condition'
require_relative 'rubocop/cop/lint/duplicate_hash_key'
Expand Down
133 changes: 133 additions & 0 deletions lib/rubocop/cop/lint/deprecated_open_ssl_constant.rb
@@ -0,0 +1,133 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Lint
# Algorithmic constants for `OpenSSL::Cipher` and `OpenSSL::Digest`
# deprecated since OpenSSL version 2.2.0. Prefer passing a string
# instead.
#
# @example
#
# # Example for OpenSSL::Cipher instantiation.
#
# # bad
# OpenSSL::Cipher::AES.new(128, :GCM)
#
# # good
# OpenSSL::Cipher.new('AES-128-GCM')
#
# @example
#
# # Example for OpenSSL::Digest instantiation.
#
# # bad
# OpenSSL::Digest::SHA256.new
#
# # good
# OpenSSL::Digest.new('SHA256')
#
# @example
#
# # Example for ::Digest inherited class methods.
#
# # bad
# OpenSSL::Digest::SHA256.digest('foo')
#
# # good
# OpenSSL::Digest.digest('SHA256', 'foo')
#
class DeprecatedOpenSSLConstant < Cop
include RangeHelp

MSG = 'Use `%<constant>s.%<method>s(%<replacement_args>s)`' \
' instead of `%<original>s`.'

def_node_matcher :algorithm_const, <<~PATTERN
(send
$(const
(const
(const {nil? cbase} :OpenSSL) {:Cipher :Digest})
_)
...)
PATTERN

def on_send(node)
add_offense(node) if algorithm_const(node)
end

def autocorrect(node)
algorithm_constant, = algorithm_const(node)

lambda do |corrector|
corrector.remove(algorithm_constant.loc.double_colon)
corrector.remove(algorithm_constant.loc.name)

corrector.replace(
correction_range(node),
"#{node.loc.selector.source}(#{replacement_args(node)})"
)
end
end

private

def message(node)
algorithm_constant, = algorithm_const(node)
parent_constant = openssl_class(algorithm_constant)
replacement_args = replacement_args(node)
method = node.loc.selector.source

format(
MSG,
constant: parent_constant,
method: method,
replacement_args: replacement_args,
original: node.source
)
end

def correction_range(node)
begin_pos = node.loc.selector.column
end_pos = node.loc.expression.last_column
range_between(begin_pos, end_pos)
end

def openssl_class(node)
node.children.first.source
end

def algorithm_name(node)
name = node.loc.name.source

if openssl_class(node) == 'OpenSSL::Cipher'
name.scan(/.{3}/).join('-')
else
name
end
end

def sanitize_arguments(arguments)
arguments.flat_map { |arg| arg.source.tr(":'", '').split('-') }
end

def replacement_args(node)
algorithm_constant, = algorithm_const(node)
algorithm_name = algorithm_name(algorithm_constant)

if openssl_class(algorithm_constant) == 'OpenSSL::Cipher'
build_cipher_arguments(node, algorithm_name)
else
(["'#{algorithm_name}'"] + node.arguments.map(&:source)).join(', ')
end
end

def build_cipher_arguments(node, algorithm_name)
algorithm_parts = algorithm_name.split('-')
size_and_mode = sanitize_arguments(node.arguments)
"'#{(algorithm_parts + size_and_mode + ['CBC']).take(3).join('-')}'"
end
end
end
end
end
1 change: 1 addition & 0 deletions manual/cops.md
Expand Up @@ -187,6 +187,7 @@ In the following section you find all available cops:
* [Lint/CircularArgumentReference](cops_lint.md#lintcircularargumentreference)
* [Lint/Debugger](cops_lint.md#lintdebugger)
* [Lint/DeprecatedClassMethods](cops_lint.md#lintdeprecatedclassmethods)
* [Lint/DeprecatedOpenSSLConstant](cops_lint.md#lintdeprecatedopensslconstant)
* [Lint/DisjunctiveAssignmentInConstructor](cops_lint.md#lintdisjunctiveassignmentinconstructor)
* [Lint/DuplicateCaseCondition](cops_lint.md#lintduplicatecasecondition)
* [Lint/DuplicateHashKey](cops_lint.md#lintduplicatehashkey)
Expand Down
40 changes: 40 additions & 0 deletions manual/cops_lint.md
Expand Up @@ -298,6 +298,46 @@ Dir.exist?(some_path)
block_given?
```

## Lint/DeprecatedOpenSSLConstant

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Pending | Yes | Yes | 0.84 | -

Algorithmic constants for `OpenSSL::Cipher` and `OpenSSL::Digest`
deprecated since OpenSSL version 2.2.0. Prefer passing a string
instead.

### Examples

```ruby
# Example for OpenSSL::Cipher instantiation.

# bad
OpenSSL::Cipher::AES.new(128, :GCM)

# good
OpenSSL::Cipher.new('AES-128-GCM')
```
```ruby
# Example for OpenSSL::Digest instantiation.

# bad
OpenSSL::Digest::SHA256.new

# good
OpenSSL::Digest.new('SHA256')
```
```ruby
# Example for ::Digest inherited class methods.

# bad
OpenSSL::Digest::SHA256.digest('foo')

# good
OpenSSL::Digest.digest('SHA256', 'foo')
```

## Lint/DisjunctiveAssignmentInConstructor

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
Expand Down
102 changes: 102 additions & 0 deletions spec/rubocop/cop/lint/deprecated_open_ssl_constant_spec.rb
@@ -0,0 +1,102 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Lint::DeprecatedOpenSSLConstant do
subject(:cop) { described_class.new(config) }

let(:config) { RuboCop::Config.new }

it 'registers an offense with cipher constant and two arguments and corrects' do
expect_offense(<<~RUBY)
OpenSSL::Cipher::AES.new(128, :GCM)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `OpenSSL::Cipher.new('AES-128-GCM')` instead of `OpenSSL::Cipher::AES.new(128, :GCM)`.
RUBY

expect_correction(<<~RUBY)
OpenSSL::Cipher.new('AES-128-GCM')
RUBY
end

it 'registers an offense with cipher constant and one argument and corrects' do
expect_offense(<<~RUBY)
OpenSSL::Cipher::AES.new('128-GCM')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `OpenSSL::Cipher.new('AES-128-GCM')` instead of `OpenSSL::Cipher::AES.new('128-GCM')`.
RUBY

expect_correction(<<~RUBY)
OpenSSL::Cipher.new('AES-128-GCM')
RUBY
end

it 'registers an offense with AES + blocksize constant and mode argument and corrects' do
expect_offense(<<~RUBY)
OpenSSL::Cipher::AES128.new(:GCM)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `OpenSSL::Cipher.new('AES-128-GCM')` instead of `OpenSSL::Cipher::AES128.new(:GCM)`.
RUBY

expect_correction(<<~RUBY)
OpenSSL::Cipher.new('AES-128-GCM')
RUBY
end

it 'registers an offense with AES + blocksize constant and corrects' do
expect_offense(<<~RUBY)
OpenSSL::Cipher::AES128.new
^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `OpenSSL::Cipher.new('AES-128-CBC')` instead of `OpenSSL::Cipher::AES128.new`.
RUBY

expect_correction(<<~RUBY)
OpenSSL::Cipher.new('AES-128-CBC')
RUBY
end

it 'does not register an offense when using cipher with a string' do
expect_no_offenses(<<~RUBY)
OpenSSL::Cipher.new('AES-128-GCM')
RUBY
end

it 'registers an offense when building an instance using an digest constant and corrects' do
expect_offense(<<~RUBY)
OpenSSL::Digest::SHA256.new
^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `OpenSSL::Digest.new('SHA256')` instead of `OpenSSL::Digest::SHA256.new`.
RUBY

expect_correction(<<~RUBY)
OpenSSL::Digest.new('SHA256')
RUBY
end

it 'registers an offense when using ::Digest class methods on an algorithm constant and corrects' do
expect_offense(<<~RUBY)
OpenSSL::Digest::SHA256.digest('foo')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `OpenSSL::Digest.digest('SHA256', 'foo')` instead of `OpenSSL::Digest::SHA256.digest('foo')`.
RUBY

expect_correction(<<~RUBY)
OpenSSL::Digest.digest('SHA256', 'foo')
RUBY
end

it 'registers an offense when using an digest constant with chained methods and corrects' do
expect_offense(<<~RUBY)
OpenSSL::Digest::SHA256.new.digest('foo')
^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `OpenSSL::Digest.new('SHA256')` instead of `OpenSSL::Digest::SHA256.new`.
RUBY

expect_correction(<<~RUBY)
OpenSSL::Digest.new('SHA256').digest('foo')
RUBY
end

it 'does not register an offense when building digest using an algorithm string' do
expect_no_offenses(<<~RUBY)
OpenSSL::Digest.new('SHA256')
RUBY
end

it 'does not register an offense when using ::Digest class methods with an algorithm string and value' do
expect_no_offenses(<<~RUBY)
OpenSSL::Digest.digest('SHA256', 'foo')
RUBY
end
end