diff --git a/Rakefile b/Rakefile index 5cc39337b..f7a6660fb 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,8 @@ +require 'open3' + require 'bundler' require 'bundler/gem_tasks' + begin Bundler.setup(:default, :development) rescue Bundler::BundlerError => e @@ -27,4 +30,19 @@ task :internal_investigation do abort('RuboCop failed!') unless result.zero? end -task default: [:spec, :internal_investigation] +desc 'Build config/default.yml' +task :build_config do + sh('bin/build_config') +end + +desc 'Confirm config/default.yml is up to date' +task confirm_config: :build_config do + _, stdout, _, process = + Open3.popen3('git diff --exit-code config/default.yml') + + unless process.value.success? + raise "default.yml is out of sync:\n\n#{stdout.read}\nRun bin/build_config" + end +end + +task default: [:build_config, :spec, :internal_investigation, :confirm_config] diff --git a/bin/build_config b/bin/build_config new file mode 100755 index 000000000..6bcfeee2f --- /dev/null +++ b/bin/build_config @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby + +$LOAD_PATH.unshift(File.join(__dir__, '..', 'lib')) + +require 'yard' + +require 'rubocop/rspec/description_extractor' +require 'rubocop/rspec/config_formatter' + +glob = File.join(__dir__, '..', 'lib', 'rubocop', 'cop', 'rspec', '*.rb') +YARD.parse(Dir[glob], []) + +descriptions = RuboCop::RSpec::DescriptionExtractor.new(YARD::Registry.all).to_h +current_config = YAML.load_file('config/default.yml') + +File.write( + 'config/default.yml', + RuboCop::RSpec::ConfigFormatter.new(current_config, descriptions).dump +) diff --git a/config/default.yml b/config/default.yml index 082c8f925..dd82d3c01 100644 --- a/config/default.yml +++ b/config/default.yml @@ -1,21 +1,20 @@ +--- AllCops: RSpec: Patterns: - # Match any file that contains `_spec.rb` so that FilePath can also flag - # and correct files with odd extensions like `spec/foo_spec.rb.html` - - '_spec.rb' - - '(?:^|/)spec/' + - _spec.rb + - "(?:^|/)spec/" RSpec/AnyInstance: - Description: 'Check that instances are not being stubbed globally' + Description: Check that instances are not being stubbed globally. Enabled: true RSpec/BeEql: - Description: 'Check for expectations where `be(...)` can be used instead of `eql(...)`' + Description: Check for expectations where `be(...)` can replace `eql(...)`. Enabled: true RSpec/HookArgument: - Description: 'Check the arguments passed to `before`, `around`, and `after`.' + Description: Checks the arguments passed to `before`, `around`, and `after`. Enabled: true EnforcedStyle: implicit SupportedStyles: @@ -24,20 +23,20 @@ RSpec/HookArgument: - example RSpec/DescribeClass: - Description: 'Check that the first argument to the top level describe is the tested class or module.' + Description: Check that the first argument to the top level describe is a constant. Enabled: true RSpec/DescribedClass: - Description: 'Use `described_class` for tested class / module' + Description: Checks that tests use `described_class`. SkipBlocks: false Enabled: true RSpec/DescribeMethod: - Description: 'Checks that the second argument to top level describe is the tested method name.' + Description: Checks that the second argument to `describe` specifies a method. Enabled: true RSpec/ExampleWording: - Description: 'Do not use should when describing your tests.' + Description: Checks that example descriptions do not start with "should". Enabled: true CustomTransform: be: is @@ -46,72 +45,72 @@ RSpec/ExampleWording: IgnoredWords: [] RSpec/EmptyExampleGroup: - Description: 'Checks for `describe` and `context` groups without tests.' + Description: Checks if an example group does not include any tests. Enabled: true CustomIncludeMethods: [] RSpec/ExpectActual: - Description: 'Checks for `expect(...)` calls containing literal values' + Description: Checks for `expect(...)` calls containing literal values. Enabled: true RSpec/MultipleDescribes: - Description: 'Checks for multiple top level describes.' + Description: Checks for multiple top level describes. Enabled: true RSpec/MultipleExpectations: - Description: 'Checks for multiple `expect(...)` calls in one example.' + Description: Checks if examples contain too many `expect` calls. Enabled: true Max: 1 RSpec/NestedGroups: - Description: 'Checks for multiple levels of context nesting.' + Description: Checks for nested example groups. Enabled: true MaxNesting: 2 RSpec/InstanceVariable: - Description: 'Checks for the usage of instance variables.' + Description: Checks for instance variable usage in specs. AssignmentOnly: false Enabled: true RSpec/LetSetup: - Description: 'Checks for `let!` being used for test setup.' + Description: Checks unreferenced `let!` calls being used for test setup. Enabled: true RSpec/LeadingSubject: - Description: 'Checks for `subject` definitions that come after `let` definitions.' + Description: Checks for `subject` definitions that come after `let` definitions. Enabled: true RSpec/FilePath: - Description: 'Checks the file and folder naming of the spec file.' + Description: Checks that spec file paths are consistent with the test subject. Enabled: true CustomTransform: RuboCop: rubocop RSpec: rspec RSpec/VerifiedDoubles: - Description: 'Prefer using verifying doubles over normal doubles.' + Description: Prefer using verifying doubles over normal doubles. Enabled: true IgnoreSymbolicNames: false RSpec/NotToNot: - Description: 'Enforces the usage of the same method on all negative message expectations.' + Description: Checks for consistent method usage for negating expectations. EnforcedStyle: not_to SupportedStyles: - - not_to - - to_not + - not_to + - to_not Enabled: true RSpec/Focus: - Description: 'Checks if there are focused specs.' + Description: Checks if examples are focused. Enabled: true RSpec/ExampleLength: - Description: 'Checks for long example' + Description: Checks for long examples. Enabled: true Max: 5 RSpec/MessageExpectation: - Description: 'Checks for consistent message expectation style.' + Description: Checks for consistent message expectation style. Enabled: true EnforcedStyle: allow SupportedStyles: @@ -119,9 +118,9 @@ RSpec/MessageExpectation: - expect RSpec/NamedSubject: - Description: 'Name your RSpec subject if you reference it explicitly' + Description: Checks for explicitly referenced test subjects. Enabled: true RSpec/SubjectStub: - Description: 'Checks for stubbed test subjects' + Description: Checks for stubbed test subjects. Enabled: true diff --git a/lib/rubocop/cop/rspec/any_instance.rb b/lib/rubocop/cop/rspec/any_instance.rb index ce2c4b013..237a54371 100644 --- a/lib/rubocop/cop/rspec/any_instance.rb +++ b/lib/rubocop/cop/rspec/any_instance.rb @@ -1,6 +1,8 @@ module RuboCop module Cop module RSpec + # Check that instances are not being stubbed globally. + # # Prefer instance doubles over stubbing any instance of a class # # @example diff --git a/lib/rubocop/cop/rspec/be_eql.rb b/lib/rubocop/cop/rspec/be_eql.rb index d47c0a986..ce3a3d89b 100644 --- a/lib/rubocop/cop/rspec/be_eql.rb +++ b/lib/rubocop/cop/rspec/be_eql.rb @@ -1,7 +1,7 @@ module RuboCop module Cop module RSpec - # Check for test expectations that can use `be` instead of `eql` + # Check for expectations where `be(...)` can replace `eql(...)`. # # The `be` matcher compares by identity while the `eql` matcher # compares using `eql?`. Integers, floats, booleans, and symbols diff --git a/lib/rubocop/cop/rspec/describe_class.rb b/lib/rubocop/cop/rspec/describe_class.rb index 2861e317b..106f0daf9 100644 --- a/lib/rubocop/cop/rspec/describe_class.rb +++ b/lib/rubocop/cop/rspec/describe_class.rb @@ -3,8 +3,7 @@ module RuboCop module Cop module RSpec - # Check that the first argument to the top level describe is the tested - # class or module. + # Check that the first argument to the top level describe is a constant. # # @example # # bad diff --git a/lib/rubocop/cop/rspec/describe_method.rb b/lib/rubocop/cop/rspec/describe_method.rb index 8416e9818..a26ea9837 100644 --- a/lib/rubocop/cop/rspec/describe_method.rb +++ b/lib/rubocop/cop/rspec/describe_method.rb @@ -3,8 +3,7 @@ module RuboCop module Cop module RSpec - # Checks that the second argument to the top level describe is the tested - # method name. + # Checks that the second argument to `describe` specifies a method. # # @example # # bad diff --git a/lib/rubocop/cop/rspec/described_class.rb b/lib/rubocop/cop/rspec/described_class.rb index 7414c18ce..24c9c8622 100644 --- a/lib/rubocop/cop/rspec/described_class.rb +++ b/lib/rubocop/cop/rspec/described_class.rb @@ -3,6 +3,8 @@ module RuboCop module Cop module RSpec + # Checks that tests use `described_class`. + # # If the first argument of describe is a class, the class is exposed to # each example via described_class - this should be used instead of # repeating the class. diff --git a/lib/rubocop/cop/rspec/empty_example_group.rb b/lib/rubocop/cop/rspec/empty_example_group.rb index 1ca7884d1..1c9279c69 100644 --- a/lib/rubocop/cop/rspec/empty_example_group.rb +++ b/lib/rubocop/cop/rspec/empty_example_group.rb @@ -3,7 +3,7 @@ module RuboCop module Cop module RSpec - # Checks if an example group does not include any tests + # Checks if an example group does not include any tests. # # This cop is configurable using the `CustomIncludeMethods` option # diff --git a/lib/rubocop/cop/rspec/example_length.rb b/lib/rubocop/cop/rspec/example_length.rb index 87e906398..2edf52c2d 100644 --- a/lib/rubocop/cop/rspec/example_length.rb +++ b/lib/rubocop/cop/rspec/example_length.rb @@ -3,6 +3,8 @@ module RuboCop module Cop module RSpec + # Checks for long examples. + # # A long example is usually more difficult to understand. Consider # extracting out some behaviour, e.g. with a `let` block, or a helper # method. diff --git a/lib/rubocop/cop/rspec/example_wording.rb b/lib/rubocop/cop/rspec/example_wording.rb index dcc2a04d5..167b202a4 100644 --- a/lib/rubocop/cop/rspec/example_wording.rb +++ b/lib/rubocop/cop/rspec/example_wording.rb @@ -3,8 +3,9 @@ module RuboCop module Cop module RSpec - # Do not use should when describing your tests. - # see: http://betterspecs.org/#should + # Checks that example descriptions do not start with "should". + # + # @see http://betterspecs.org/#should # # The autocorrect is experimental - use with care! It can be configured # with CustomTransform (e.g. have => has) and IgnoredWords (e.g. only). diff --git a/lib/rubocop/cop/rspec/expect_actual.rb b/lib/rubocop/cop/rspec/expect_actual.rb index 381bed35b..a644d38fd 100644 --- a/lib/rubocop/cop/rspec/expect_actual.rb +++ b/lib/rubocop/cop/rspec/expect_actual.rb @@ -3,7 +3,7 @@ module RuboCop module Cop module RSpec - # Checks for literal values within `expect(...)` + # Checks for `expect(...)` calls containing literal values. # # @example # # bad diff --git a/lib/rubocop/cop/rspec/file_path.rb b/lib/rubocop/cop/rspec/file_path.rb index 5b29e5b10..64e4cfa14 100644 --- a/lib/rubocop/cop/rspec/file_path.rb +++ b/lib/rubocop/cop/rspec/file_path.rb @@ -3,6 +3,8 @@ module RuboCop module Cop module RSpec + # Checks that spec file paths are consistent with the test subject. + # # Checks the path of the spec file and enforces that it reflects the # described class/module and its optionally called out method. # diff --git a/lib/rubocop/cop/rspec/focus.rb b/lib/rubocop/cop/rspec/focus.rb index 601e0c985..8c1c30d6b 100644 --- a/lib/rubocop/cop/rspec/focus.rb +++ b/lib/rubocop/cop/rspec/focus.rb @@ -3,7 +3,7 @@ module RuboCop module Cop module RSpec - # Checks if test is focused. + # Checks if examples are focused. # # @example # # bad diff --git a/lib/rubocop/cop/rspec/hook_argument.rb b/lib/rubocop/cop/rspec/hook_argument.rb index ccba67dec..370ffeffd 100644 --- a/lib/rubocop/cop/rspec/hook_argument.rb +++ b/lib/rubocop/cop/rspec/hook_argument.rb @@ -3,6 +3,8 @@ module RuboCop module Cop module RSpec + # Checks the arguments passed to `before`, `around`, and `after`. + # # This cop checks for consistent style when specifying RSpec # hooks which run for each example. There are three supported # styles: "implicit", "each", and "example." All styles have diff --git a/lib/rubocop/cop/rspec/instance_variable.rb b/lib/rubocop/cop/rspec/instance_variable.rb index d4e735015..0396c2146 100644 --- a/lib/rubocop/cop/rspec/instance_variable.rb +++ b/lib/rubocop/cop/rspec/instance_variable.rb @@ -3,8 +3,7 @@ module RuboCop module Cop module RSpec - # When you have to assign a variable instead of using an instance - # variable, use let. + # Checks for instance variable usage in specs. # # This cop can be configured with the option `AssignmentOnly` which # will configure the cop to only register offenses on instance diff --git a/lib/rubocop/cop/rspec/leading_subject.rb b/lib/rubocop/cop/rspec/leading_subject.rb index 614714f80..c9b813afe 100644 --- a/lib/rubocop/cop/rspec/leading_subject.rb +++ b/lib/rubocop/cop/rspec/leading_subject.rb @@ -3,7 +3,7 @@ module RuboCop module Cop module RSpec - # Declare test subject before let declarations + # Checks for `subject` definitions that come after `let` definitions. # # @example # # bad diff --git a/lib/rubocop/cop/rspec/let_setup.rb b/lib/rubocop/cop/rspec/let_setup.rb index a268a7d76..82415c392 100644 --- a/lib/rubocop/cop/rspec/let_setup.rb +++ b/lib/rubocop/cop/rspec/let_setup.rb @@ -3,7 +3,7 @@ module RuboCop module Cop module RSpec - # Detect unreferenced `let!` calls being used for test setup + # Checks unreferenced `let!` calls being used for test setup. # # @example # # Bad diff --git a/lib/rubocop/cop/rspec/message_expectation.rb b/lib/rubocop/cop/rspec/message_expectation.rb index 93af3940b..6b0f0be4c 100644 --- a/lib/rubocop/cop/rspec/message_expectation.rb +++ b/lib/rubocop/cop/rspec/message_expectation.rb @@ -3,7 +3,7 @@ module RuboCop module Cop module RSpec - # Checks specs for consistent message expectation style + # Checks for consistent message expectation style. # # This cop can be configured in your configuration using the # `EnforcedStyle` option and supports `--auto-gen-config`. diff --git a/lib/rubocop/cop/rspec/multiple_describes.rb b/lib/rubocop/cop/rspec/multiple_describes.rb index 577d9ad73..a0b51a6a3 100644 --- a/lib/rubocop/cop/rspec/multiple_describes.rb +++ b/lib/rubocop/cop/rspec/multiple_describes.rb @@ -3,8 +3,10 @@ module RuboCop module Cop module RSpec - # Checks for multiple top level describes. They should be nested if it is - # for the same class or module or separated into different files. + # Checks for multiple top level describes. + # + # Multiple descriptions for the same class or module should either + # be nested or separated into different test files. # # @example # # bad diff --git a/lib/rubocop/cop/rspec/multiple_expectations.rb b/lib/rubocop/cop/rspec/multiple_expectations.rb index 31936e6f5..9b12e15c8 100644 --- a/lib/rubocop/cop/rspec/multiple_expectations.rb +++ b/lib/rubocop/cop/rspec/multiple_expectations.rb @@ -3,7 +3,7 @@ module RuboCop module Cop module RSpec - # Checks if examples contain too many `expect` calls + # Checks if examples contain too many `expect` calls. # # @see http://betterspecs.org/#single Single expectation test # diff --git a/lib/rubocop/cop/rspec/named_subject.rb b/lib/rubocop/cop/rspec/named_subject.rb index 42135eae3..c1c3d2e99 100644 --- a/lib/rubocop/cop/rspec/named_subject.rb +++ b/lib/rubocop/cop/rspec/named_subject.rb @@ -3,7 +3,14 @@ module RuboCop module Cop module RSpec - # Give `subject` a descriptive name if you reference it directly + # Checks for explicitly referenced test subjects. + # + # RSpec lets you declare an "implicit subject" using `subject { ... }` + # which allows for tests like `it { should be_valid }`. If you need to + # reference your test subject you should explicitly name it using + # `subject(:your_subject_name) { ... }`. Your test subjects should be + # the most important object in your tests so they deserve a descriptive + # name. # # @example # # bad diff --git a/lib/rubocop/cop/rspec/nested_groups.rb b/lib/rubocop/cop/rspec/nested_groups.rb index 0357cf939..56ffd20f5 100644 --- a/lib/rubocop/cop/rspec/nested_groups.rb +++ b/lib/rubocop/cop/rspec/nested_groups.rb @@ -3,7 +3,7 @@ module RuboCop module Cop module RSpec - # Checks for nested example groups + # Checks for nested example groups. # # This cop is configurable using the `MaxNesting` option # diff --git a/lib/rubocop/cop/rspec/not_to_not.rb b/lib/rubocop/cop/rspec/not_to_not.rb index 4e150b6aa..6af913799 100644 --- a/lib/rubocop/cop/rspec/not_to_not.rb +++ b/lib/rubocop/cop/rspec/not_to_not.rb @@ -1,8 +1,7 @@ module RuboCop module Cop module RSpec - # Enforces the usage of the same method on all negative message - # expectations. + # Checks for consistent method usage for negating expectations. # # @example # # bad diff --git a/lib/rubocop/cop/rspec/subject_stub.rb b/lib/rubocop/cop/rspec/subject_stub.rb index f6341a140..c06008846 100644 --- a/lib/rubocop/cop/rspec/subject_stub.rb +++ b/lib/rubocop/cop/rspec/subject_stub.rb @@ -3,7 +3,7 @@ module RuboCop module Cop module RSpec - # Checks for stubs on test subjects + # Checks for stubbed test subjects. # # @see https://robots.thoughtbot.com/don-t-stub-the-system-under-test # diff --git a/lib/rubocop/cop/rspec/verified_doubles.rb b/lib/rubocop/cop/rspec/verified_doubles.rb index fb41709a6..d06f815ee 100644 --- a/lib/rubocop/cop/rspec/verified_doubles.rb +++ b/lib/rubocop/cop/rspec/verified_doubles.rb @@ -4,7 +4,8 @@ module RuboCop module Cop module RSpec # Prefer using verifying doubles over normal doubles. - # see: https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles + # + # @see https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles # # @example # # bad diff --git a/lib/rubocop/rspec/config_formatter.rb b/lib/rubocop/rspec/config_formatter.rb new file mode 100644 index 000000000..44818dfea --- /dev/null +++ b/lib/rubocop/rspec/config_formatter.rb @@ -0,0 +1,33 @@ +require 'yaml' + +module RuboCop + module RSpec + # Builds a YAML config file from two config hashes + class ConfigFormatter + NAMESPACE = 'RSpec'.freeze + + def initialize(config, descriptions) + @config = config + @descriptions = descriptions + end + + def dump + YAML.dump(unified_config).gsub(/^#{NAMESPACE}/, "\n#{NAMESPACE}") + end + + private + + def unified_config + cops.each_with_object(config.dup) do |cop, unified| + unified[cop] = config.fetch(cop).merge(descriptions.fetch(cop)) + end + end + + def cops + (descriptions.keys + config.keys).uniq.grep(/\A#{NAMESPACE}/) + end + + attr_reader :config, :descriptions + end + end +end diff --git a/lib/rubocop/rspec/description_extractor.rb b/lib/rubocop/rspec/description_extractor.rb new file mode 100644 index 000000000..7aec05cc8 --- /dev/null +++ b/lib/rubocop/rspec/description_extractor.rb @@ -0,0 +1,35 @@ +module RuboCop + module RSpec + # Extracts cop descriptions from YARD docstrings + class DescriptionExtractor + COP_NAMESPACE = 'RuboCop::Cop::RSpec'.freeze + COP_FORMAT = 'RSpec/%s'.freeze + + def initialize(yardocs) + @yardocs = yardocs + end + + def to_h + cop_documentation.each_with_object({}) do |(name, docstring), config| + config[format(COP_FORMAT, name)] = { + 'Description' => docstring.split("\n\n").first.to_s + } + end + end + + private + + def cop_documentation + yardocs + .select(&method(:cop?)) + .map { |doc| [doc.name, doc.docstring] } + end + + def cop?(doc) + doc.type.equal?(:class) && doc.to_s.start_with?(COP_NAMESPACE) + end + + attr_reader :yardocs + end + end +end diff --git a/rubocop-rspec.gemspec b/rubocop-rspec.gemspec index 9b7f75f2d..7082e7793 100644 --- a/rubocop-rspec.gemspec +++ b/rubocop-rspec.gemspec @@ -38,4 +38,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'anima' spec.add_development_dependency 'concord' spec.add_development_dependency 'adamantium' + spec.add_development_dependency 'yard' end diff --git a/spec/project/default_config_spec.rb b/spec/project/default_config_spec.rb index 50ba00662..1aa7d5207 100644 --- a/spec/project/default_config_spec.rb +++ b/spec/project/default_config_spec.rb @@ -42,6 +42,10 @@ def cop_configuration(config_key) end end + it 'ends every description with a period' do + expect(cop_configuration('Description')).to all(end_with('.')) + end + it 'includes Enabled: true for every cop' do expect(cop_configuration('Enabled')).to all(be(true)) end diff --git a/spec/rubocop/rspec/config_formatter_spec.rb b/spec/rubocop/rspec/config_formatter_spec.rb new file mode 100644 index 000000000..61c521608 --- /dev/null +++ b/spec/rubocop/rspec/config_formatter_spec.rb @@ -0,0 +1,48 @@ +require 'rubocop/rspec/config_formatter' + +RSpec.describe RuboCop::RSpec::ConfigFormatter do + let(:config) do + { + 'AllCops' => { + 'Setting' => 'fourty two' + }, + 'RSpec/Foo' => { + 'Config' => 2, + 'Enabled' => true + }, + 'RSpec/Bar' => { + 'Enabled' => true + } + } + end + + let(:descriptions) do + { + 'RSpec/Foo' => { + 'Description' => 'Blah' + }, + 'RSpec/Bar' => { + 'Description' => 'Wow' + } + } + end + + it 'builds a YAML dump with spacing between cops' do + formatter = described_class.new(config, descriptions) + + expect(formatter.dump).to eql(<<-YAML.gsub(/^\s+\|/, '')) + |--- + |AllCops: + | Setting: fourty two + | + |RSpec/Foo: + | Config: 2 + | Enabled: true + | Description: Blah + | + |RSpec/Bar: + | Enabled: true + | Description: Wow + YAML + end +end diff --git a/spec/rubocop/rspec/description_extractor_spec.rb b/spec/rubocop/rspec/description_extractor_spec.rb new file mode 100644 index 000000000..c4fb1cd14 --- /dev/null +++ b/spec/rubocop/rspec/description_extractor_spec.rb @@ -0,0 +1,35 @@ +require 'yard' + +require 'rubocop/rspec/description_extractor' + +RSpec.describe RuboCop::RSpec::DescriptionExtractor do + let(:yardocs) do + [ + instance_double( + YARD::CodeObjects::MethodObject, + docstring: "Checks foo\n\nLong description", + to_s: 'RuboCop::Cop::RSpec::Foo', + type: :class, + name: 'Foo' + ), + instance_double( + YARD::CodeObjects::MethodObject, + docstring: 'Hi', + to_s: 'RuboCop::Cop::RSpec::Foo#bar', + type: :method, + name: 'Foo#bar' + ), + instance_double( + YARD::CodeObjects::MethodObject, + docstring: 'This is not a cop', + to_s: 'RuboCop::Cop::Mixin::Sneaky', + type: :class + ) + ] + end + + it 'builds a hash of descriptions' do + expect(described_class.new(yardocs).to_h) + .to eql('RSpec/Foo' => { 'Description' => 'Checks foo' }) + end +end