diff --git a/.github/workflows/code_style_checks.yaml b/.github/workflows/code_style_checks.yaml index 780c83e..14b2c41 100644 --- a/.github/workflows/code_style_checks.yaml +++ b/.github/workflows/code_style_checks.yaml @@ -22,7 +22,7 @@ jobs: os: - ubuntu ruby: - - "2.7" + - "3.0" runs-on: ${{ matrix.os }}-latest steps: - name: Checkout diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 251f5c0..1454991 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -22,17 +22,11 @@ jobs: os: - ubuntu ruby: - - "2.1" - - "2.2" - - "2.3" - - "2.4" - - "2.5" - - "2.6" - - "2.7" + - "3.0" test_command: ["bundle exec rspec && bundle exec cucumber"] include: - os: ubuntu - ruby: "2.4.2" + ruby: "3.0" test_command: "bundle exec rspec" runs-on: ${{ matrix.os }}-latest steps: diff --git a/.rubocop.yml b/.rubocop.yml index 49aa730..0a4457d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,12 +1,17 @@ inherit_from: .rubocop_todo.yml AllCops: - TargetRubyVersion: 1.9 + TargetRubyVersion: 3.0 DisplayCopNames: true + NewCops: disable Exclude: + - "benchmarks/**/*" - "tmp/**/*" - "vendor/**/*" + - "script/**/*.rb" + - "spec/fixtures/*.rb" - "spec/ruby_version_specific/*.rb" + - "spec/*.rb" # forces method defs to have params in parens Style/MethodDefParentheses: @@ -38,7 +43,7 @@ Lint/UnusedMethodArgument: Enabled: false # changes x ** 2 to x**2 -Style/SpaceAroundOperators: +Layout/SpaceAroundOperators: Enabled: false # doesn't allow vars starting with _ @@ -56,7 +61,7 @@ Style/Documentation: # enforces line length of 80 # TODO enable -Metrics/LineLength: +Layout/LineLength: Enabled: false # triggered by Contract ({ :name => String, :age => Fixnum }) => nil @@ -101,7 +106,7 @@ Lint/DuplicateMethods: Style/TrivialAccessors: Enabled: false -Style/MultilineOperationIndentation: +Layout/MultilineOperationIndentation: EnforcedStyle: indented # Asks you to use %w{array of words} if possible. @@ -111,12 +116,12 @@ Style/WordArray: # conflicts with contracts # we define contracts like `Baz = 1` -Style/ConstantName: +Naming/ConstantName: Enabled: false # `Contract` violates this, otherwise a good cop (enforces snake_case method names) # TODO possible to get this enabled but ignore `Contract`? -Style/MethodName: +Naming/MethodName: Enabled: false # checks for !! @@ -129,9 +134,30 @@ Metrics/ParameterLists: Enabled: false # Checks that braces used for hash literals have or don't have surrounding space depending on configuration. -Style/SpaceInsideHashLiteralBraces: +Layout/SpaceInsideHashLiteralBraces: Enabled: false # TODO enable Style/SpecialGlobalVars: Enabled: false + +Style/IfUnlessModifier: + Enabled: false + +Naming/MemoizedInstanceVariableName: + Enabled: false + +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent + +Layout/HashAlignment: + EnforcedColonStyle: table + +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: consistent_comma + +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: consistent_comma + +Style/TrailingCommaInArguments: + EnforcedStyleForMultiline: consistent_comma diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1f2767c..2005c69 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,6 +6,9 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. +require: + - rubocop-performance + # Offense count: 2 Lint/NonLocalExitFromIterator: Exclude: @@ -32,14 +35,14 @@ Style/Alias: # Offense count: 1 # Cop supports --auto-correct. # Configuration parameters: AllowAdjacentOneLineDefs. -Style/EmptyLineBetweenDefs: +Layout/EmptyLineBetweenDefs: Exclude: - 'benchmarks/wrap_test.rb' # Offense count: 1 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. -Style/ExtraSpacing: +Layout/ExtraSpacing: Exclude: - 'spec/builtin_contracts_spec.rb' @@ -58,7 +61,7 @@ Style/IfInsideElse: # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: symmetrical, new_line, same_line -Style/MultilineHashBraceLayout: +Layout/MultilineHashBraceLayout: Exclude: - 'spec/contracts_spec.rb' - 'spec/fixtures/fixtures.rb' @@ -130,6 +133,6 @@ Style/TrailingUnderscoreVariable: # Offense count: 1 # Cop supports --auto-correct. -Style/UnneededInterpolation: +Style/RedundantInterpolation: Exclude: - 'lib/contracts/formatters.rb' diff --git a/Gemfile b/Gemfile index af86768..540761b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,17 +1,21 @@ +# frozen_string_literal: true + source "https://rubygems.org" gemspec group :test do - gem "rspec" gem "aruba" gem "cucumber", "~> 1.3.20" - gem "rubocop", "~> 0.41.2" if RUBY_VERSION >= "2" + gem "rspec" + + gem "rubocop", ">= 1.0.0" + gem "rubocop-performance", ">= 1.0.0" end group :development do - gem "relish" gem "method_profiler" - gem "ruby-prof" gem "rake" + gem "relish" + gem "ruby-prof" end diff --git a/Rakefile b/Rakefile index 439b075..fd4014c 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + task :default => [:spec] task :add_tag do diff --git a/contracts.gemspec b/contracts.gemspec index 1b5d5a1..9310e61 100644 --- a/contracts.gemspec +++ b/contracts.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require File.expand_path(File.join(__FILE__, "../lib/contracts/version")) Gem::Specification.new do |s| @@ -10,6 +12,7 @@ Gem::Specification.new do |s| s.files = `git ls-files`.split("\n") s.homepage = "https://github.com/egonSchiele/contracts.ruby" s.license = "BSD-2-Clause" + s.required_ruby_version = [">= 3.0", "< 4"] s.post_install_message = " 0.16.x will be the supporting Ruby 2.x and be feature frozen (only fixes will be released) For Ruby 3.x use 0.17.x or later (might not be released yet) diff --git a/features/support/env.rb b/features/support/env.rb index f22aa9f..55e18c5 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "aruba/cucumber" require "aruba/jruby" if RUBY_PLATFORM == "java" diff --git a/lib/contracts.rb b/lib/contracts.rb index a4303d0..e8bab85 100644 --- a/lib/contracts.rb +++ b/lib/contracts.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "contracts/attrs" require "contracts/builtin_contracts" require "contracts/decorators" @@ -51,7 +53,9 @@ class Contract < Contracts::Decorator end attr_reader :args_contracts, :ret_contract, :klass, :method + def initialize(klass, method, *contracts) + super(klass, method) unless contracts.last.is_a?(Hash) unless contracts.one? fail %{ @@ -93,17 +97,17 @@ def initialize(klass, method, *contracts) last_contract = args_contracts.last penultimate_contract = args_contracts[-2] @has_options_contract = if @has_proc_contract - penultimate_contract.is_a?(Hash) || penultimate_contract.is_a?(Contracts::Builtin::KeywordArgs) + penultimate_contract.is_a?(Contracts::Builtin::KeywordArgs) else - last_contract.is_a?(Hash) || last_contract.is_a?(Contracts::Builtin::KeywordArgs) + last_contract.is_a?(Contracts::Builtin::KeywordArgs) end # === @klass, @method = klass, method end - def pretty_contract c - c.is_a?(Class) ? c.name : c.class.name + def pretty_contract contract + contract.is_a?(Class) ? contract.name : contract.class.name end def to_s @@ -130,15 +134,15 @@ def self.failure_msg(data) expected_prefix = "Expected: " expected_value = Contracts::Support.indent_string( Contracts::Formatters::Expected.new(data[:contract]).contract.pretty_inspect, - expected_prefix.length + expected_prefix.length, ).strip - expected_line = expected_prefix + expected_value + "," + expected_line = "#{expected_prefix}#{expected_value}," # Actual actual_prefix = "Actual: " actual_value = Contracts::Support.indent_string( data[:arg].pretty_inspect, - actual_prefix.length + actual_prefix.length, ).strip actual_line = actual_prefix + actual_value @@ -157,16 +161,19 @@ def self.failure_msg(data) position_value = Contracts::Support.method_position(data[:method]) position_line = position_prefix + position_value - header + - "\n" + + [ + header, Contracts::Support.indent_string( - [expected_line, - actual_line, - value_line, - contract_line, - position_line].join("\n"), - indent_amount - ) + [ + expected_line, + actual_line, + value_line, + contract_line, + position_line, + ].join("\n"), + indent_amount, + ), + ].join("\n") end # Callback for when a contract fails. By default it raises @@ -182,7 +189,7 @@ def self.failure_msg(data) # puts failure_msg(data) # exit # end - def self.failure_callback(data, use_pattern_matching = true) + def self.failure_callback(data, use_pattern_matching: true) if data[:contracts].pattern_match? && use_pattern_matching return DEFAULT_FAILURE_CALLBACK.call(data) end @@ -242,19 +249,21 @@ def call(*args, &blk) # returns true if it appended nil def maybe_append_block! args, blk return false unless @has_proc_contract && !blk && - (@args_contract_index || args.size < args_contracts.size) + (@args_contract_index || args.size < args_contracts.size) + args << nil true end # Same thing for when we have named params but didn't pass any in. # returns true if it appended nil - def maybe_append_options! args, blk + def maybe_append_options! args, kargs, blk return false unless @has_options_contract - if @has_proc_contract && (args_contracts[-2].is_a?(Hash) || args_contracts[-2].is_a?(Contracts::Builtin::KeywordArgs)) && !args[-2].is_a?(Hash) - args.insert(-2, {}) - elsif (args_contracts[-1].is_a?(Hash) || args_contracts[-1].is_a?(Contracts::Builtin::KeywordArgs)) && !args[-1].is_a?(Hash) - args << {} + + if @has_proc_contract && args_contracts[-2].is_a?(Contracts::Builtin::KeywordArgs) + args.insert(-2, kargs) + elsif args_contracts[-1].is_a?(Contracts::Builtin::KeywordArgs) + args << kargs end true end diff --git a/lib/contracts/attrs.rb b/lib/contracts/attrs.rb index 320837f..e32a7e1 100644 --- a/lib/contracts/attrs.rb +++ b/lib/contracts/attrs.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Contracts module Attrs def attr_reader_with_contract(*names, contract) diff --git a/lib/contracts/builtin_contracts.rb b/lib/contracts/builtin_contracts.rb index 25d36b0..43fd293 100644 --- a/lib/contracts/builtin_contracts.rb +++ b/lib/contracts/builtin_contracts.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "contracts/formatters" require "set" @@ -30,35 +32,35 @@ def self.valid? val # Check that an argument is a positive number. class Pos def self.valid? val - val && val.is_a?(Numeric) && val > 0 + val.is_a?(Numeric) && val.positive? end end # Check that an argument is a negative number. class Neg def self.valid? val - val && val.is_a?(Numeric) && val < 0 + val.is_a?(Numeric) && val.negative? end end # Check that an argument is an +Integer+. class Int def self.valid? val - val && val.is_a?(Integer) + val.is_a?(Integer) end end # Check that an argument is a natural number (includes zero). class Nat def self.valid? val - val && val.is_a?(Integer) && val >= 0 + val.is_a?(Integer) && val >= 0 end end # Check that an argument is a positive natural number (excludes zero). class NatPos def self.valid? val - val && val.is_a?(Integer) && val > 0 + val.is_a?(Integer) && val.positive? end end @@ -96,6 +98,7 @@ def self.[](*vals) # Example: Or[Fixnum, Float] class Or < CallableClass def initialize(*vals) + super() @vals = vals end @@ -107,9 +110,11 @@ def valid?(val) end def to_s + # rubocop:disable Style/StringConcatenation @vals[0, @vals.size-1].map do |x| InspectWrapper.create(x) end.join(", ") + " or " + InspectWrapper.create(@vals[-1]).to_s + # rubocop:enable Style/StringConcatenation end end @@ -118,6 +123,7 @@ def to_s # Example: Xor[Fixnum, Float] class Xor < CallableClass def initialize(*vals) + super() @vals = vals end @@ -130,9 +136,11 @@ def valid?(val) end def to_s + # rubocop:disable Style/StringConcatenation @vals[0, @vals.size-1].map do |x| InspectWrapper.create(x) end.join(", ") + " xor " + InspectWrapper.create(@vals[-1]).to_s + # rubocop:enable Style/StringConcatenation end end @@ -141,6 +149,7 @@ def to_s # Example: And[Fixnum, Float] class And < CallableClass def initialize(*vals) + super() @vals = vals end @@ -152,9 +161,11 @@ def valid?(val) end def to_s + # rubocop:disable Style/StringConcatenation @vals[0, @vals.size-1].map do |x| InspectWrapper.create(x) end.join(", ") + " and " + InspectWrapper.create(@vals[-1]).to_s + # rubocop:enable Style/StringConcatenation end end @@ -164,6 +175,7 @@ def to_s # Example: RespondTo[:password, :credit_card] class RespondTo < CallableClass def initialize(*meths) + super() @meths = meths end @@ -185,6 +197,7 @@ def to_s # Example: Send[:valid?] class Send < CallableClass def initialize(*meths) + super() @meths = meths end @@ -204,11 +217,12 @@ def to_s # Example: Exactly[Numeric] class Exactly < CallableClass def initialize(cls) + super() @cls = cls end def valid?(val) - val.class == @cls + val.instance_of?(@cls) end def to_s @@ -222,6 +236,7 @@ def to_s # Example: Enum[:a, :b, :c]? class Enum < CallableClass def initialize(*vals) + super() @vals = vals end @@ -235,6 +250,7 @@ def valid?(val) # Example: Eq[Class] class Eq < CallableClass def initialize(value) + super() @value = value end @@ -252,6 +268,7 @@ def to_s # Example: Not[nil] class Not < CallableClass def initialize(*vals) + super() @vals = vals end @@ -275,12 +292,14 @@ def to_s # Example: CollectionOf[Array, Num] class CollectionOf < CallableClass def initialize(collection_class, contract) + super() @collection_class = collection_class @contract = contract end def valid?(vals) return false unless vals.is_a?(@collection_class) + vals.all? do |val| res, _ = Contract.valid?(val, @contract) res @@ -298,7 +317,7 @@ def initialize(collection_class, &before_new) end def new(contract) - @before_new && @before_new.call + @before_new&.call CollectionOf.new(@collection_class, contract) end @@ -324,7 +343,9 @@ def new(contract) # Example: Args[Or[String, Num]] class Args < CallableClass attr_reader :contract + def initialize(contract) + super() @contract = contract end @@ -343,6 +364,7 @@ def self.valid? val # Example: RangeOf[Nat], RangeOf[Date], ... class RangeOf < CallableClass def initialize(contract) + super() @contract = contract end @@ -364,6 +386,7 @@ class HashOf < CallableClass INVALID_KEY_VALUE_PAIR = "You should provide only one key-value pair to HashOf contract" def initialize(key, value = nil) + super() if value @key = key @value = value @@ -376,6 +399,7 @@ def initialize(key, value = nil) def valid?(hash) return false unless hash.is_a?(Hash) + keys_match = hash.keys.map { |k| Contract.valid?(k, @key) }.all? vals_match = hash.values.map { |v| Contract.valid?(v, @value) }.all? @@ -400,6 +424,7 @@ class StrictHash < CallableClass attr_reader :contract_hash def initialize(contract_hash) + super() @contract_hash = contract_hash end @@ -417,12 +442,14 @@ def valid?(arg) # Example: KeywordArgs[ e: Range, f: Optional[Num] ] class KeywordArgs < CallableClass def initialize(options) + super() @options = options end def valid?(hash) return false unless hash.is_a?(Hash) return false unless hash.keys - options.keys == [] + options.all? do |key, contract| Optional._valid?(hash, key, contract) end @@ -445,6 +472,7 @@ def inspect # Example: DescendantOf[ e: Range, f: Optional[Num] ] class DescendantOf < CallableClass def initialize(parent_class) + super() @parent_class = parent_class end @@ -473,11 +501,13 @@ class Optional < CallableClass def self._valid?(hash, key, contract) return Contract.valid?(hash[key], contract) unless contract.is_a?(Optional) + contract.within_opt_hash! !hash.key?(key) || Contract.valid?(hash[key], contract) end def initialize(contract) + super() @contract = contract @within_opt_hash = false end @@ -506,6 +536,7 @@ def inspect def ensure_within_opt_hash return if within_opt_hash + fail ArgumentError, UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH end @@ -531,7 +562,9 @@ def include_proc? # Example: Func[Num => Num] # the function should take a number and return a number class Func < CallableClass attr_reader :contracts + def initialize(*contracts) + super() @contracts = contracts end end diff --git a/lib/contracts/call_with.rb b/lib/contracts/call_with.rb index 9252c79..eff5687 100644 --- a/lib/contracts/call_with.rb +++ b/lib/contracts/call_with.rb @@ -1,17 +1,19 @@ +# frozen_string_literal: true + module Contracts module CallWith - def call_with(this, *args, &blk) - call_with_inner(false, this, *args, &blk) + def call_with(this, *args, **kargs, &blk) + call_with_inner(false, this, *args, **kargs, &blk) end - def call_with_inner(returns, this, *args, &blk) + def call_with_inner(returns, this, *args, **kargs, &blk) args << blk if blk # Explicitly append blk=nil if nil != Proc contract violation anticipated nil_block_appended = maybe_append_block!(args, blk) # Explicitly append options={} if Hash contract is present - maybe_append_options!(args, blk) + kargs_appended = maybe_append_options!(args, kargs, blk) # Loop forward validating the arguments up to the splat (if there is one) (@args_contract_index || args.size).times do |i| @@ -20,14 +22,16 @@ def call_with_inner(returns, this, *args, &blk) validator = @args_validators[i] unless validator && validator[arg] - data = {:arg => arg, - :contract => contract, - :class => klass, - :method => method, - :contracts => self, - :arg_pos => i+1, - :total_args => args.size, - :return_value => false} + data = { + arg: arg, + contract: contract, + class: klass, + method: method, + contracts: self, + arg_pos: i+1, + total_args: args.size, + return_value: false, + } return ParamContractError.new("as return value", data) if returns return unless Contract.failure_callback(data) end @@ -57,14 +61,18 @@ def call_with_inner(returns, this, *args, &blk) validator = @args_validators[args_contracts.size - 1 - j] unless validator && validator[arg] - return unless Contract.failure_callback(:arg => arg, - :contract => contract, - :class => klass, - :method => method, - :contracts => self, - :arg_pos => i-1, - :total_args => args.size, - :return_value => false) + # rubocop:disable Style/SoleNestedConditional + return unless Contract.failure_callback({ + :arg => arg, + :contract => contract, + :class => klass, + :method => method, + :contracts => self, + :arg_pos => i - 1, + :total_args => args.size, + :return_value => false, + }) + # rubocop:enable Style/SoleNestedConditional end if contract.is_a?(Contracts::Func) @@ -76,24 +84,27 @@ def call_with_inner(returns, this, *args, &blk) # If we put the block into args for validating, restore the args # OR if we added a fake nil at the end because a block wasn't passed in. args.slice!(-1) if blk || nil_block_appended + args.slice!(-1) if kargs_appended result = if method.respond_to?(:call) # proc, block, lambda, etc - method.call(*args, &blk) + method.call(*args, **kargs, &blk) else # original method name reference # Don't reassign blk, else Travis CI shows "stack level too deep". target_blk = blk - target_blk = lambda { |*params| blk.call(*params) } if blk && blk.is_a?(Contract) - method.send_to(this, *args, &target_blk) + target_blk = lambda { |*params| blk.call(*params) } if blk.is_a?(Contract) + method.send_to(this, *args, **kargs, &target_blk) end unless @ret_validator[result] - Contract.failure_callback(:arg => result, - :contract => ret_contract, - :class => klass, - :method => method, - :contracts => self, - :return_value => true) + Contract.failure_callback({ + arg: result, + contract: ret_contract, + class: klass, + method: method, + contracts: self, + return_value: true, + }) end this.verify_invariants!(method) if this.respond_to?(:verify_invariants!) diff --git a/lib/contracts/core.rb b/lib/contracts/core.rb index 176b873..d251236 100644 --- a/lib/contracts/core.rb +++ b/lib/contracts/core.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Contracts module Core def self.included(base) @@ -25,7 +27,7 @@ def functype(funcname) # NOTE: Workaround for `defined?(super)` bug in ruby 1.9.2 # source: http://stackoverflow.com/a/11181685 # bug: https://bugs.ruby-lang.org/issues/6644 - base.class_eval <<-RUBY + base.class_eval <<-RUBY, __FILE__, __LINE__ + 1 # TODO: deprecate # Required when contracts are included in global scope def Contract(*args) diff --git a/lib/contracts/decorators.rb b/lib/contracts/decorators.rb index 11cd7b5..d436df6 100644 --- a/lib/contracts/decorators.rb +++ b/lib/contracts/decorators.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Contracts module MethodDecorators def self.extended(klass) @@ -25,6 +27,7 @@ class Decorator class << self; attr_accessor :decorators; end def self.inherited(klass) + super name = klass.name.gsub(/^./) { |m| m.downcase } return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/ @@ -33,11 +36,11 @@ def self.inherited(klass) # make a new method that is the name of your decorator. # that method accepts random args and a block. # inside, `decorate` is called with those params. - MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1 + MethodDecorators.module_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{klass}(*args, &blk) ::Contracts::Engine.fetch_from(self).decorate(#{klass}, *args, &blk) end - ruby_eval + RUBY_EVAL end def initialize(klass, method) diff --git a/lib/contracts/engine.rb b/lib/contracts/engine.rb index 063a397..c047637 100644 --- a/lib/contracts/engine.rb +++ b/lib/contracts/engine.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "contracts/engine/base" require "contracts/engine/target" require "contracts/engine/eigenclass" diff --git a/lib/contracts/engine/base.rb b/lib/contracts/engine/base.rb index cd86630..4b45763 100644 --- a/lib/contracts/engine/base.rb +++ b/lib/contracts/engine/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Contracts module Engine # Contracts engine @@ -90,7 +92,7 @@ def add_method_decorator(type, name, decorator) def nearest_decorated_ancestor current = klass current_engine = self - ancestors = current.ancestors[1..-1] + ancestors = current.ancestors[1..] while current && current_engine && !current_engine.decorated_methods? current = ancestors.shift @@ -109,8 +111,7 @@ def decorated_methods end # No-op because it is safe to add decorators to normal classes - def validate! - end + def validate!; end def pop_decorators decorators.tap { clear_decorators } diff --git a/lib/contracts/engine/eigenclass.rb b/lib/contracts/engine/eigenclass.rb index 7011631..98f9956 100644 --- a/lib/contracts/engine/eigenclass.rb +++ b/lib/contracts/engine/eigenclass.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Contracts module Engine # Special case of contracts engine for eigenclasses @@ -27,8 +29,7 @@ def self.lift(eigenclass, owner) end # No-op for eigenclasses - def set_eigenclass_owner - end + def set_eigenclass_owner; end # Fetches just eigenclasses decorators def all_decorators diff --git a/lib/contracts/engine/target.rb b/lib/contracts/engine/target.rb index e8a0a84..dba096b 100644 --- a/lib/contracts/engine/target.rb +++ b/lib/contracts/engine/target.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Contracts module Engine # Represents class in question diff --git a/lib/contracts/errors.rb b/lib/contracts/errors.rb index c224905..4060668 100644 --- a/lib/contracts/errors.rb +++ b/lib/contracts/errors.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # @private # Base class for Contract errors # @@ -65,6 +67,7 @@ class << self alias_method :to_s, :message def initialize(message = DEFAULT_MESSAGE) + super @message = message end end diff --git a/lib/contracts/formatters.rb b/lib/contracts/formatters.rb index 2d4f70b..3ee27bc 100644 --- a/lib/contracts/formatters.rb +++ b/lib/contracts/formatters.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "pp" module Contracts @@ -7,18 +9,19 @@ module Formatters class Expected # @param full [Boolean] if false only unique `to_s` values will be output, # non unique values become empty string. - def initialize(contract, full = true) + def initialize(contract, full: true) @contract, @full = contract, full end # Formats any type of Contract. def contract(contract = @contract) - if contract.is_a?(Hash) + case contract + when Hash hash_contract(contract) - elsif contract.is_a?(Array) + when Array array_contract(contract) else - InspectWrapper.create(contract, @full) + InspectWrapper.create(contract, full: @full) end end @@ -26,14 +29,14 @@ def contract(contract = @contract) def hash_contract(hash) @full = true # Complex values output completely, overriding @full hash.inject({}) do |repr, (k, v)| - repr.merge(k => InspectWrapper.create(contract(v), @full)) + repr.merge(k => InspectWrapper.create(contract(v), full: @full)) end end # Formats Array contracts. def array_contract(array) @full = true - array.map { |v| InspectWrapper.create(contract(v), @full) } + array.map { |v| InspectWrapper.create(contract(v), full: @full) } end end @@ -42,8 +45,8 @@ def array_contract(array) module InspectWrapper # InspectWrapper is a factory, will never be an instance # @return [ClassInspectWrapper, ObjectInspectWrapper] - def self.create(value, full = true) - if value.class == Class + def self.create(value, full: true) + if value.instance_of?(Class) ClassInspectWrapper else ObjectInspectWrapper @@ -66,6 +69,7 @@ def inspect return @value.inspect if empty_val? return @value.to_s if plain? return delim(@value.to_s) if useful_to_s? + useful_inspect end @@ -96,7 +100,7 @@ def plain? end def useful_to_s? - # Useless to_s value or no custom to_s behavious defined + # Useless to_s value or no custom to_s behaviour defined !empty_to_s? && custom_to_s? end @@ -125,7 +129,7 @@ class ObjectInspectWrapper include InspectWrapper def custom_to_s? - !@value.to_s.match(/#\<\w+:.+\>/) + !@value.to_s.match(/#<\w+:.+>/) end def useful_inspect diff --git a/lib/contracts/invariants.rb b/lib/contracts/invariants.rb index 56d2d82..4dec30d 100644 --- a/lib/contracts/invariants.rb +++ b/lib/contracts/invariants.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Contracts module Invariants def self.included(base) @@ -46,10 +48,12 @@ def expected def check_on(target, method) return if target.instance_eval(&@condition) - self.class.failure_callback(:expected => expected, - :actual => false, - :target => target, - :method => method) + self.class.failure_callback({ + expected: expected, + actual: false, + target: target, + method: method, + }) end def self.failure_callback(data) diff --git a/lib/contracts/method_handler.rb b/lib/contracts/method_handler.rb index d57e37e..5378e6a 100644 --- a/lib/contracts/method_handler.rb +++ b/lib/contracts/method_handler.rb @@ -1,15 +1,17 @@ +# frozen_string_literal: true + module Contracts # Handles class and instance methods addition # Represents single such method class MethodHandler METHOD_REFERENCE_FACTORY = { :class_methods => SingletonMethodReference, - :instance_methods => MethodReference + :instance_methods => MethodReference, } RAW_METHOD_STRATEGY = { :class_methods => lambda { |target, name| target.method(name) }, - :instance_methods => lambda { |target, name| target.instance_method(name) } + :instance_methods => lambda { |target, name| target.instance_method(name) }, } # Creates new instance of MethodHandler @@ -78,11 +80,13 @@ def decorated_methods def pattern_matching? return @_pattern_matching if defined?(@_pattern_matching) + @_pattern_matching = decorated_methods.any? { |x| x.method != method_reference } end def mark_pattern_matching_decorators return unless pattern_matching? + decorated_methods.each(&:pattern_match!) end @@ -107,13 +111,13 @@ def redefine_method current_engine = engine # We are gonna redefine original method here - method_reference.make_definition(target) do |*args, &blk| + method_reference.make_definition(target) do |*args, **kargs, &blk| engine = current_engine.nearest_decorated_ancestor # If we weren't able to find any ancestor that has decorated methods # FIXME : this looks like untested code (commenting it out doesn't make specs red) unless engine - fail "Couldn't find decorator for method " + self.class.name + ":#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case." + fail "Couldn't find decorator for method #{self.class.name}:#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case." end # Fetch decorated methods out of the contracts engine @@ -130,17 +134,20 @@ def redefine_method last_error = nil decorated_methods.each do |decorated_method| - result = decorated_method.call_with_inner(true, self, *args, &blk) + result = decorated_method.call_with_inner(true, self, *args, **kargs, &blk) return result unless result.is_a?(ParamContractError) + last_error = result end begin - if ::Contract.failure_callback(last_error.data, false) - decorated_methods.last.call_with_inner(false, self, *args, &blk) + if ::Contract.failure_callback(last_error&.data, use_pattern_matching: false) + decorated_methods.last.call_with_inner(false, self, *args, **kargs, &blk) end + # rubocop:disable Naming/RescuedExceptionsVariableName rescue expected_error => final_error raise final_error.to_contract_error + # rubocop:enable Naming/RescuedExceptionsVariableName end end end @@ -173,7 +180,8 @@ def validate_pattern_matching! return if matched.empty? - fail ContractError.new(%{ + fail ContractError.new( + %{ It looks like you are trying to use pattern-matching, but multiple definitions for function '#{method_name}' have the same contract for input parameters: @@ -181,7 +189,9 @@ def validate_pattern_matching! #{(matched + [decorator]).map(&:to_s).join("\n")} Each definition needs to have a different contract for the parameters. - }, {}) + }, + {}, + ) end end end diff --git a/lib/contracts/method_reference.rb b/lib/contracts/method_reference.rb index 0bc68f8..f2c09b7 100644 --- a/lib/contracts/method_reference.rb +++ b/lib/contracts/method_reference.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Contracts # MethodReference represents original method reference that was # decorated by contracts.ruby. Used for instance methods. @@ -39,8 +41,8 @@ def make_alias(this) # Calls original method on specified `this` argument with # specified arguments `args` and block `&blk`. - def send_to(this, *args, &blk) - this.send(aliased_name, *args, &blk) + def send_to(this, *args, **kargs, &blk) + this.send(aliased_name, *args, **kargs, &blk) end private diff --git a/lib/contracts/support.rb b/lib/contracts/support.rb index cda3a55..0e8922e 100644 --- a/lib/contracts/support.rb +++ b/lib/contracts/support.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Contracts module Support class << self @@ -8,7 +10,7 @@ def method_position(method) if file.nil? || line.nil? "" else - file + ":" + line.to_s + "#{file}:#{line}" end end @@ -45,7 +47,7 @@ def eigenclass?(target) def indent_string(string, amount) string.gsub( /^(?!$)/, - (string[/^[ \t]/] || " ") * amount + (string[/^[ \t]/] || " ") * amount, ) end diff --git a/lib/contracts/validators.rb b/lib/contracts/validators.rb index b006e78..e9d7d6a 100644 --- a/lib/contracts/validators.rb +++ b/lib/contracts/validators.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Contracts module Validators DEFAULT_VALIDATOR_STRATEGIES = { @@ -9,6 +11,7 @@ module Validators Array => lambda do |contract| lambda do |arg| return false unless arg.is_a?(Array) && arg.length == contract.length + arg.zip(contract).all? do |_arg, _contract| Contract.valid?(_arg, _contract) end @@ -19,6 +22,7 @@ module Validators Hash => lambda do |contract| lambda do |arg| return false unless arg.is_a?(Hash) + contract.keys.all? do |k| Contract.valid?(arg[k], contract[k]) end @@ -59,7 +63,7 @@ module Validators :default => lambda do |contract| lambda { |arg| contract == arg } - end + end, }.freeze # Allows to override validator with custom one. @@ -90,7 +94,7 @@ def make_validator!(contract) else if contract.respond_to? :valid? :valid - elsif klass == Class || klass == Module + elsif [Class, Module].include?(klass) :class else :default diff --git a/lib/contracts/version.rb b/lib/contracts/version.rb index f268776..01ae3af 100644 --- a/lib/contracts/version.rb +++ b/lib/contracts/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Contracts VERSION = "0.16.1" end diff --git a/spec/builtin_contracts_spec.rb b/spec/builtin_contracts_spec.rb index 00cf495..a9f7257 100644 --- a/spec/builtin_contracts_spec.rb +++ b/spec/builtin_contracts_spec.rb @@ -376,10 +376,6 @@ def passes(&some) fails { @o.hash_keywordargs(:hash => nil) } fails { @o.hash_keywordargs(:hash => 1) } end - - it "should pass if a method is overloaded with non-KeywordArgs" do - passes { @o.person_keywordargs("name", 10) } - end end describe "Optional:" do @@ -405,15 +401,15 @@ def something(hash) end context "given a fulfilled contract" do - it { expect(@o.gives_max_value(:panda => 1, :bamboo => 2)).to eq(2) } - it { expect(@o.pretty_gives_max_value(:panda => 1, :bamboo => 2)).to eq(2) } + it { expect(@o.gives_max_value({ :panda => 1, :bamboo => 2 })).to eq(2) } + it { expect(@o.pretty_gives_max_value({ :panda => 1, :bamboo => 2 })).to eq(2) } end context "given an unfulfilled contract" do - it { fails { @o.gives_max_value(:panda => "1", :bamboo => "2") } } + it { fails { @o.gives_max_value({ :panda => "1", :bamboo => "2" }) } } it { fails { @o.gives_max_value(nil) } } it { fails { @o.gives_max_value(1) } } - it { fails { @o.pretty_gives_max_value(:panda => "1", :bamboo => "2") } } + it { fails { @o.pretty_gives_max_value({ :panda => "1", :bamboo => "2" }) } } end describe "#to_s" do @@ -430,25 +426,25 @@ def something(hash) describe "StrictHash:" do context "when given an exact correct input" do it "does not raise an error" do - passes { @o.strict_person(:name => "calvin", :age => 10) } + passes { @o.strict_person({ :name => "calvin", :age => 10 }) } end end context "when given an input with correct keys but wrong types" do it "raises an error" do - fails { @o.strict_person(:name => "calvin", :age => "10") } + fails { @o.strict_person({ :name => "calvin", :age => "10" }) } end end context "when given an input with missing keys" do it "raises an error" do - fails { @o.strict_person(:name => "calvin") } + fails { @o.strict_person({ :name => "calvin" }) } end end context "when given an input with extra keys" do it "raises an error" do - fails { @o.strict_person(:name => "calvin", :age => 10, :soft => true) } + fails { @o.strict_person({ :name => "calvin", :age => 10, :soft => true }) } end end diff --git a/spec/contracts_spec.rb b/spec/contracts_spec.rb index af3df57..edc138f 100644 --- a/spec/contracts_spec.rb +++ b/spec/contracts_spec.rb @@ -349,19 +349,19 @@ def self.greeting(name) describe "Hashes" do it "should pass for exact correct input" do - expect { @o.person(:name => "calvin", :age => 10) }.to_not raise_error + expect { @o.person({ :name => "calvin", :age => 10 }) }.to_not raise_error end it "should pass even if some keys don't have contracts" do - expect { @o.person(:name => "calvin", :age => 10, :foo => "bar") }.to_not raise_error + expect { @o.person({ :name => "calvin", :age => 10, :foo => "bar" }) }.to_not raise_error end it "should fail if a key with a contract on it isn't provided" do - expect { @o.person(:name => "calvin") }.to raise_error(ContractError) + expect { @o.person({ :name => "calvin" }) }.to raise_error(ContractError) end it "should fail for incorrect input" do - expect { @o.person(:name => 50, :age => 10) }.to raise_error(ContractError) + expect { @o.person({ :name => 50, :age => 10 }) }.to raise_error(ContractError) end end @@ -612,16 +612,19 @@ def delim(match) it "should contain to_s representation within a Hash contract" do expect do - @o.hash_complex_contracts(:rigged => "bad") + @o.hash_complex_contracts({ :rigged => "bad" }) end.to raise_error(ContractError, not_s(delim "TrueClass or FalseClass")) end it "should contain to_s representation within a nested Hash contract" do expect do - @o.nested_hash_complex_contracts(:rigged => true, - :contents => { - :kind => 0, - :total => 42 }) + @o.nested_hash_complex_contracts({ + :rigged => true, + :contents => { + :kind => 0, + :total => 42, + }, + }) end.to raise_error(ContractError, not_s(delim "String or Symbol")) end diff --git a/spec/fixtures/fixtures.rb b/spec/fixtures/fixtures.rb index 67b5740..55638c2 100644 --- a/spec/fixtures/fixtures.rb +++ b/spec/fixtures/fixtures.rb @@ -120,16 +120,11 @@ def nested_hash_complex_contracts(data) end Contract C::KeywordArgs[:name => String, :age => Fixnum] => nil - def person_keywordargs(data) - end - - # Testing overloaded method - Contract String, Fixnum => nil - def person_keywordargs(name, age) + def person_keywordargs(name: "name", age: 10) end Contract C::KeywordArgs[:hash => C::HashOf[Symbol, C::Num]] => nil - def hash_keywordargs(data) + def hash_keywordargs(hash:) end Contract (/foo/) => nil diff --git a/spec/override_validators_spec.rb b/spec/override_validators_spec.rb index 293c84c..25af373 100644 --- a/spec/override_validators_spec.rb +++ b/spec/override_validators_spec.rb @@ -30,15 +30,15 @@ def something(opts) obj = klass.new expect do - obj.something(:a => 35, :b => "hello") + obj.something({ :a => 35, :b => "hello" }) end.to raise_error(ContractError) expect do - obj.something( + obj.something({ :a => 35, :b => "hello", :it_is_a_hash => true - ) + }) end.not_to raise_error end diff --git a/spec/ruby_version_specific/contracts_spec_2.0.rb b/spec/ruby_version_specific/contracts_spec_2.0.rb index 78c5e69..c2b3d69 100644 --- a/spec/ruby_version_specific/contracts_spec_2.0.rb +++ b/spec/ruby_version_specific/contracts_spec_2.0.rb @@ -1,10 +1,10 @@ class GenericExample - Contract C::Args[String], { repeat: C::Maybe[C::Num] } => C::ArrayOf[String] + Contract C::Args[String], C::KeywordArgs[ repeat: C::Maybe[C::Num] ] => C::ArrayOf[String] def splat_then_optional_named(*vals, repeat: 2) vals.map { |v| v * repeat } end - Contract ({foo: C::Nat}) => nil + Contract C::KeywordArgs[ foo: C::Nat ] => nil def nat_test_with_kwarg(foo: 10) end diff --git a/spec/validators_spec.rb b/spec/validators_spec.rb index 588580e..22dc2a9 100644 --- a/spec/validators_spec.rb +++ b/spec/validators_spec.rb @@ -34,7 +34,7 @@ describe "within a hash" do it "should pass for a matching string" do - expect { o.hash_containing_foo(:host => "foo.example.org") }.to_not raise_error + expect { o.hash_containing_foo({ :host => "foo.example.org" }) }.to_not raise_error end end