From ff4f73b9df8d5abc9e1a47b7eda23d5bea2a561f Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Tue, 19 Dec 2023 12:38:56 +0100 Subject: [PATCH 1/5] Move config injection to own file --- lib/rubocop/markdown.rb | 1 + lib/rubocop/markdown/inject.rb | 18 ++++++++++++++++++ lib/rubocop/markdown/rubocop_ext.rb | 13 +------------ 3 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 lib/rubocop/markdown/inject.rb diff --git a/lib/rubocop/markdown.rb b/lib/rubocop/markdown.rb index 915bf32..af5da64 100644 --- a/lib/rubocop/markdown.rb +++ b/lib/rubocop/markdown.rb @@ -9,6 +9,7 @@ module Markdown PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze CONFIG_DEFAULT = PROJECT_ROOT.join("config", "default.yml").freeze + require_relative "markdown/inject" require_relative "markdown/preprocess" require_relative "markdown/rubocop_ext" if defined?(::RuboCop::ProcessedSource) end diff --git a/lib/rubocop/markdown/inject.rb b/lib/rubocop/markdown/inject.rb new file mode 100644 index 0000000..3a49d59 --- /dev/null +++ b/lib/rubocop/markdown/inject.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module RuboCop + module Markdown + # Merge markdown config into default configuration + # See https://github.com/rubocop/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb + module Inject # :nodoc: + def self.defaults! + path = CONFIG_DEFAULT.to_s + hash = ConfigLoader.send(:load_yaml_configuration, path) + config = RuboCop::Config.new(hash, path) + puts "configuration from #{path}" if ConfigLoader.debug? + config = ConfigLoader.merge_with_default(config, path) + ConfigLoader.instance_variable_set(:@default_configuration, config) + end + end + end +end diff --git a/lib/rubocop/markdown/rubocop_ext.rb b/lib/rubocop/markdown/rubocop_ext.rb index 1283b97..d7fa0ca 100644 --- a/lib/rubocop/markdown/rubocop_ext.rb +++ b/lib/rubocop/markdown/rubocop_ext.rb @@ -26,17 +26,6 @@ module Markdown # :nodoc: class << self attr_accessor :config_store - # Merge markdown config into default configuration - # See https://github.com/backus/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb - def inject! - path = CONFIG_DEFAULT.to_s - hash = ConfigLoader.send(:load_yaml_configuration, path) - config = Config.new(hash, path) - puts "configuration from #{path}" if ConfigLoader.debug? - config = ConfigLoader.merge_with_default(config, path) - ConfigLoader.instance_variable_set(:@default_configuration, config) - end - def markdown_file?(file) MARKDOWN_EXTENSIONS.include?(File.extname(file)) end @@ -44,7 +33,7 @@ def markdown_file?(file) end end -RuboCop::Markdown.inject! +RuboCop::Markdown::Inject.defaults! RuboCop::Runner.prepend(Module.new do # Set config store for Markdown From 14ea3f6d0dc519e9be2ca6b1abe948622837ead2 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Tue, 19 Dec 2023 12:59:17 +0100 Subject: [PATCH 2/5] Use rubocop template api to extract markdown code blocks --- lib/rubocop/markdown.rb | 4 +- lib/rubocop/markdown/preprocess.rb | 84 +++++++-------- lib/rubocop/markdown/rubocop_ext.rb | 95 ----------------- lib/rubocop/markdown/ruby_extractor.rb | 48 +++++++++ test/preprocess_test.rb | 136 ++++++++----------------- test/test_helper.rb | 2 - 6 files changed, 130 insertions(+), 239 deletions(-) delete mode 100644 lib/rubocop/markdown/rubocop_ext.rb create mode 100644 lib/rubocop/markdown/ruby_extractor.rb diff --git a/lib/rubocop/markdown.rb b/lib/rubocop/markdown.rb index af5da64..295f4eb 100644 --- a/lib/rubocop/markdown.rb +++ b/lib/rubocop/markdown.rb @@ -11,6 +11,8 @@ module Markdown require_relative "markdown/inject" require_relative "markdown/preprocess" - require_relative "markdown/rubocop_ext" if defined?(::RuboCop::ProcessedSource) + require_relative "markdown/ruby_extractor" + + RuboCop::Markdown::Inject.defaults! end end diff --git a/lib/rubocop/markdown/preprocess.rb b/lib/rubocop/markdown/preprocess.rb index 1210103..eefc68e 100644 --- a/lib/rubocop/markdown/preprocess.rb +++ b/lib/rubocop/markdown/preprocess.rb @@ -4,8 +4,8 @@ module RuboCop module Markdown - # Transform source Markdown file into valid Ruby file - # by commenting out all non-code lines + # Transform markdown into multiple ProcessedSources with offsets + # from the original markdown for further use in RuboCop class Preprocess # This is a regexp to parse code blocks from .md files. # @@ -17,11 +17,8 @@ class Preprocess ([\w[[:blank:]]+]*)?\n # Match the code block syntax ([\s\S]+?) # Match everything inside the code block (^[[:blank:]]*\1[[:blank:]]*\n?) # Match closing backticks - |(^.*$) # If we are not in a codeblock, match the whole line /x.freeze - MARKER = "<--rubocop/md-->" - # See https://github.com/github/linguist/blob/v5.3.3/lib/linguist/languages.yml#L3925 RUBY_TYPES = %w[ ruby @@ -32,56 +29,51 @@ class Preprocess rbx ].freeze - class << self - # Revert preprocess changes. - # - # When autocorrect is applied, RuboCop re-writes the file - # using preproccessed source buffer. - # - # We have to restore it. - def restore_and_save!(file) - contents = File.read(file) - restore!(contents) - File.write(file, contents) - end - - def restore!(src) - src.gsub!(/^##{MARKER}/m, "") - end - end - - attr_reader :config + attr_reader :original_processed_source - def initialize(file) - @config = Markdown.config_store.for(file) + def initialize(original_processed_source) + @original_processed_source = original_processed_source end # rubocop:disable Metrics/MethodLength - def call(src) - src.gsub(MD_REGEXP) do |full_match| + def call + original_processed_source.raw_source.to_enum(:scan, MD_REGEXP).map do m = Regexp.last_match open_backticks = m[1] syntax = m[2] code = m[3] - close_backticks = m[4] - markdown = m[5] - - if markdown - # We got markdown outside of a codeblock - comment_lines(markdown) - elsif ruby_codeblock?(syntax, code) - # The codeblock we parsed is assumed ruby, keep as is and append markers to backticks - "#{comment_lines(open_backticks + syntax)}\n#{code}#{comment_lines(close_backticks)}" - else - # The codeblock is not relevant, comment it out - comment_lines(full_match) - end - end + + next unless ruby_codeblock?(syntax, code) + + # The codeblock we parsed is assumed ruby + code_indent = open_backticks.index("`") + { + offset: m.begin(3) + code_indent, + processed_source: new_processed_source(code, code_indent, original_processed_source) + } + end.compact end # rubocop:enable Metrics/MethodLength private + def new_processed_source(code, code_indent, original_processed_source) + processed_source = RuboCop::ProcessedSource.new( + strip_indent(code, code_indent), + original_processed_source.ruby_version, + original_processed_source.path + ) + + processed_source.config = original_processed_source.config + processed_source.registry = original_processed_source.registry + processed_source + end + + # Strip indentation from code inside codeblocks + def strip_indent(code, code_indent) + code.gsub(/^[[:blank:]]{#{code_indent}}/, "") + end + def ruby_codeblock?(syntax, src) maybe_ruby?(syntax) && valid_syntax?(syntax, src) end @@ -97,6 +89,10 @@ def ruby?(syntax) RUBY_TYPES.include?(syntax) end + def config + original_processed_source.config + end + # Try to parse with Ripper # Invalid Ruby code (or non-Ruby) returns `nil`. # Return true if it's explicit Ruby and warn_invalid? @@ -116,10 +112,6 @@ def warn_invalid? def autodetect? config["Markdown"]&.fetch("Autodetect", true) end - - def comment_lines(src) - src.gsub(/^/, "##{MARKER}") - end end end end diff --git a/lib/rubocop/markdown/rubocop_ext.rb b/lib/rubocop/markdown/rubocop_ext.rb deleted file mode 100644 index d7fa0ca..0000000 --- a/lib/rubocop/markdown/rubocop_ext.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -module RuboCop - module Markdown # :nodoc: - # According to Linguist. mdx was dropped but is being kept for backwards compatibility. - # See https://github.com/github-linguist/linguist/blob/8c380f360ce00b95fa08d14ce0ebccd481af1b33/lib/linguist/languages.yml#L4088-L4098 - # Keep in sync with config/default.yml - MARKDOWN_EXTENSIONS = %w[ - .md - .livemd - .markdown - .mdown - .mdwn - .mdx - .mkd - .mkdn - .mkdown - .ronn - .scd - .workbook - ].freeze - - # A list of cops that could produce offenses in commented lines - MARKDOWN_OFFENSE_COPS = %w[Lint/Syntax].freeze - - class << self - attr_accessor :config_store - - def markdown_file?(file) - MARKDOWN_EXTENSIONS.include?(File.extname(file)) - end - end - end -end - -RuboCop::Markdown::Inject.defaults! - -RuboCop::Runner.prepend(Module.new do - # Set config store for Markdown - def get_processed_source(*args) - RuboCop::Markdown.config_store = @config_store unless RuboCop::Markdown.config_store - - super - end - - # Do not cache markdown files, 'cause cache doesn't know about processing. - # NOTE: we should involve preprocessing in RuboCop::CachedData#deserialize_offenses - def file_offense_cache(file) - return yield if RuboCop::Markdown.markdown_file?(file) - - super - end - - def file_finished(file, offenses) - return super unless RuboCop::Markdown.markdown_file?(file) - - # Run Preprocess.restore if file has been autocorrected - if @options[:auto_correct] || @options[:autocorrect] - RuboCop::Markdown::Preprocess.restore_and_save!(file) - end - - super(file, offenses) - end -end) - -RuboCop::Cop::Commissioner::InvestigationReport.prepend(Module.new do - # Skip offenses reported for ignored MD source (trailing whitespaces, etc.) - def offenses - @offenses ||= begin - marker_comment = "##{RuboCop::Markdown::Preprocess::MARKER}" - offenses_per_cop.flatten(1).reject do |offense| - next if RuboCop::Markdown::MARKDOWN_OFFENSE_COPS.include?(offense.cop_name) - - offense.location.source_line.start_with?(marker_comment) - end - end - end -end) - -# Allow Rubocop to analyze markdown files -RuboCop::TargetFinder.prepend(Module.new do - def ruby_file?(file) - super || RuboCop::Markdown.markdown_file?(file) - end -end) - -# Extend ProcessedSource#parse with pre-processing -RuboCop::ProcessedSource.prepend(Module.new do - def parse(src, *args) - # only process Markdown files - src = RuboCop::Markdown::Preprocess.new(path).call(src) if - path && RuboCop::Markdown.markdown_file?(path) - super(src, *args) - end -end) diff --git a/lib/rubocop/markdown/ruby_extractor.rb b/lib/rubocop/markdown/ruby_extractor.rb new file mode 100644 index 0000000..250d116 --- /dev/null +++ b/lib/rubocop/markdown/ruby_extractor.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module RuboCop + module Markdown + # Used by RuboCop to get parsed ruby from markdown + class RubyExtractor + # According to Linguist. mdx was dropped but is being kept for backwards compatibility. + # See https://github.com/github-linguist/linguist/blob/8c380f360ce00b95fa08d14ce0ebccd481af1b33/lib/linguist/languages.yml#L4088-L4098 + # Keep in sync with config/default.yml + MARKDOWN_EXTENSIONS = %w[ + .md + .livemd + .markdown + .mdown + .mdwn + .mdx + .mkd + .mkdn + .mkdown + .ronn + .scd + .workbook + ].freeze + + class << self + def call(processed_source) + new(processed_source).call + end + end + + def initialize(processed_source) + @processed_source = processed_source + end + + def call + return unless markdown_file? + + Preprocess.new(@processed_source).call + end + + private + + def markdown_file? + MARKDOWN_EXTENSIONS.include?(File.extname(@processed_source.path || "")) + end + end + end +end diff --git a/test/preprocess_test.rb b/test/preprocess_test.rb index 90a692e..e2b4e65 100644 --- a/test/preprocess_test.rb +++ b/test/preprocess_test.rb @@ -3,12 +3,19 @@ require "test_helper" class RuboCop::Markdown::PreprocessTest < Minitest::Test - def subject(warn_invalid: false) - obj = RuboCop::Markdown::Preprocess.new("test.md") + def subject(source, warn_invalid: false) + dummy_processed_source = RuboCop::ProcessedSource.new(source, 2.6, "test.md") + dummy_processed_source.config = RuboCop::ConfigStore.new.for("test.md") + obj = RuboCop::Markdown::Preprocess.new(dummy_processed_source) obj.define_singleton_method(:warn_invalid?) { warn_invalid } obj end + def assert_parsed(raw_source, parsed, source_code) + assert_equal source_code, parsed[:processed_source].raw_source, "Expected the processed_source to contain the code block" + assert_equal raw_source.index(source_code, parsed[:offset]), parsed[:offset], "Expected the offset to start at the code block" + end + def test_no_code_snippets source = <<~SOURCE # Header @@ -16,13 +23,7 @@ def test_no_code_snippets Boby text SOURCE - expected = <<~SOURCE - #<--rubocop/md--># Header - #<--rubocop/md--> - #<--rubocop/md-->Boby text - SOURCE - - assert_equal expected, subject.call(source) + assert_equal 0, subject(source).call.size end def test_with_one_snippet @@ -40,21 +41,17 @@ def test_valid ``` SOURCE - expected = <<~SOURCE - #<--rubocop/md--># Header - #<--rubocop/md--> - #<--rubocop/md-->Code example: - #<--rubocop/md--> - #<--rubocop/md-->``` + code_block = <<~SOURCE class Test < Minitest::Test def test_valid assert false end end - #<--rubocop/md-->``` SOURCE - assert_equal expected, subject.call(source) + parsed = subject(source).call + assert_equal 1, parsed.size + assert_parsed source, parsed.first, code_block end def test_only_snippet @@ -68,17 +65,17 @@ def test_valid ``` SOURCE - expected = <<~SOURCE - #<--rubocop/md-->``` + code_block = <<~SOURCE class Test < Minitest::Test def test_valid assert false end end - #<--rubocop/md-->``` SOURCE - assert_equal expected, subject.call(source) + parsed = subject(source).call + assert_equal 1, parsed.size + assert_parsed source, parsed.first, code_block end def test_many_snippets @@ -106,31 +103,24 @@ def test_valid ``` SOURCE - expected = <<~SOURCE - #<--rubocop/md--># Header - #<--rubocop/md--> - #<--rubocop/md-->Code example: - #<--rubocop/md--> - #<--rubocop/md-->``` + code_block1 = <<~SOURCE class Test < Minitest::Test def test_valid assert false end end - #<--rubocop/md-->``` - #<--rubocop/md--> - #<--rubocop/md-->More texts and lists: - #<--rubocop/md-->- One - #<--rubocop/md-->- Two - #<--rubocop/md--> - #<--rubocop/md-->```ruby + SOURCE + + code_block2 = <<~SOURCE require "minitest/pride" require "minitest/autorun" - #<--rubocop/md-->``` SOURCE - assert_equal expected, subject.call(source) + parsed = subject(source).call + assert_equal 2, parsed.size + assert_parsed source, parsed[0], code_block1 + assert_parsed source, parsed[1], code_block2 end def test_invalid_syntax @@ -148,21 +138,7 @@ def test_valid ``` SOURCE - expected = <<~SOURCE - #<--rubocop/md--># Header - #<--rubocop/md--> - #<--rubocop/md-->Code example: - #<--rubocop/md--> - #<--rubocop/md-->``` - #<--rubocop/md-->class Test < Minitest::Test - #<--rubocop/md--> def test_valid - #<--rubocop/md--> ... - #<--rubocop/md--> end - #<--rubocop/md-->end - #<--rubocop/md-->``` - SOURCE - - assert_equal expected, subject.call(source) + assert_equal 0, subject(source).call.size end def test_non_ruby_snippet @@ -177,18 +153,7 @@ def test_non_ruby_snippet ``` SOURCE - expected = <<~SOURCE - #<--rubocop/md--># Header - #<--rubocop/md--> - #<--rubocop/md-->Code example: - #<--rubocop/md--> - #<--rubocop/md-->``` - #<--rubocop/md-->-module(evlms). - #<--rubocop/md-->-export([martians/0, martians/1]). - #<--rubocop/md-->``` - SOURCE - - assert_equal expected, subject.call(source) + assert_equal 0, subject(source).call.size end def test_ambigious_non_ruby_snippet @@ -197,7 +162,7 @@ def test_ambigious_non_ruby_snippet ```ruby it "is doing heavy stuff", :rprof do - ... + ... # Syntax error end ``` @@ -216,31 +181,15 @@ def test_ambigious_non_ruby_snippet ``` SOURCE - expected = <<~SOURCE - #<--rubocop/md--># Header - #<--rubocop/md--> - #<--rubocop/md-->```ruby - #<--rubocop/md-->it "is doing heavy stuff", :rprof do - #<--rubocop/md--> ... - #<--rubocop/md-->end - #<--rubocop/md-->``` - #<--rubocop/md--> - #<--rubocop/md-->Code example: - #<--rubocop/md--> - #<--rubocop/md-->```sh - #<--rubocop/md-->TEST_RUBY_PROF=call_stack bundle exec rake test - #<--rubocop/md-->``` - #<--rubocop/md--> - #<--rubocop/md-->Or in your code: - #<--rubocop/md--> - #<--rubocop/md-->```ruby + code_block = <<~SOURCE TestProf::RubyProf.configure do |config| config.printer = :call_stack end - #<--rubocop/md-->``` SOURCE - assert_equal expected, subject.call(source) + parsed = subject(source).call + assert_equal 1, parsed.size + assert_parsed source, parsed.first, code_block end def test_snippet_with_unclosed_backtick @@ -258,20 +207,17 @@ def test_snippet_with_unclosed_backtick ``` SOURCE - expected = <<~SOURCE - #<--rubocop/md--># Code example: - #<--rubocop/md--> - #<--rubocop/md-->```ruby + code_block1 = <<~SOURCE `method_call - #<--rubocop/md-->``` - #<--rubocop/md--> - #<--rubocop/md--># Other code example - #<--rubocop/md--> - #<--rubocop/md-->```ruby + SOURCE + + code_block2 = <<~SOURCE method_call - #<--rubocop/md-->``` SOURCE - assert_equal expected, subject(warn_invalid: true).call(source) + parsed = subject(source, warn_invalid: true).call + assert_equal 2, parsed.size + assert_parsed source, parsed[0], code_block1 + assert_parsed source, parsed[1], code_block2 end end diff --git a/test/test_helper.rb b/test/test_helper.rb index fa89018..1ee558f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -14,5 +14,3 @@ require "rubocop_assertions" require "markdown_assertions" require "rubocop-md" - -RuboCop::Markdown.config_store = RuboCop::ConfigStore.new From 9b5260dce35ccac4316384d01232c980f7df5b97 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Sat, 9 Mar 2024 11:23:26 +0100 Subject: [PATCH 3/5] Adapt offenses tests to template api Get rid of the imported rubocop assertions. This use case is now different enough that these just don't make sense anymore --- test/markdown_assertions.rb | 121 ++++++++++++++++------------ test/offense_test.rb | 30 ++++--- test/rubocop_assertions.rb | 152 ------------------------------------ test/test_helper.rb | 1 - 4 files changed, 85 insertions(+), 219 deletions(-) delete mode 100644 test/rubocop_assertions.rb diff --git a/test/markdown_assertions.rb b/test/markdown_assertions.rb index 65dadc6..f4f497f 100644 --- a/test/markdown_assertions.rb +++ b/test/markdown_assertions.rb @@ -1,19 +1,10 @@ # frozen_string_literal: true -require "fileutils" +require "rubocop/rspec/expect_offense" module RuboCop module Markdown - # Necessary overwrites over rubocop minitest assertions to run all cops and handle markdown autocorrection class Test < Minitest::Test - include AssertOffense - - class DummyCop - def initialize - @options = {} - end - end - # Lint/Syntax has a multiline offense which is impossible to match against class RuboCop::Cop::Lint::Syntax def add_offense_from_diagnostic(diagnostic, _ruby_version) @@ -21,75 +12,105 @@ def add_offense_from_diagnostic(diagnostic, _ruby_version) end end - def assert_offense(source, file = "test.md", **replacements) - @cop = DummyCop.new - super - end + def assert_offense(source, file = "test.md") + expected_annotations = RuboCop::RSpec::ExpectOffense::AnnotatedSource.parse(source) + @original_processed_source = parse_source(expected_annotations.plain_source, file) + @team, offenses = _investigate(@original_processed_source) - def assert_no_offenses(source, file = "test.md") - super + actual_annotations = expected_annotations.with_offense_annotations(offenses) + assert_equal(expected_annotations, actual_annotations) end # rubocop:disable Metrics/AbcSize - def assert_correction(correction, loop: true) - raise "`assert_correction` must follow `assert_offense`" unless @processed_source + def assert_correction(source) + raise "`assert_correction` must follow `assert_offense`" unless @original_processed_source + if autocorrect_from_team(@original_processed_source) == @original_processed_source.raw_source + raise "Use `expect_no_corrections` if the code will not change" + end iteration = 0 + processed_source = @original_processed_source new_source = loop do iteration += 1 - corrected_source = @last_corrector.rewrite + corrected_source = autocorrect_from_team(processed_source) - break corrected_source unless loop - if @last_corrector.empty? || corrected_source == @processed_source.buffer.source - break corrected_source - end + break corrected_source if corrected_source == processed_source.buffer.source if iteration > RuboCop::Runner::MAX_ITERATIONS - raise RuboCop::Runner::InfiniteCorrectionLoop.new(@processed_source.path, []) + raise RuboCop::Runner::InfiniteCorrectionLoop.new(processed_source.path, [@offenses]) end # Prepare for next loop - RuboCop::Markdown::Preprocess.restore!(corrected_source) - @processed_source = parse_source!(corrected_source) - - _investigate(@cop, @processed_source) + processed_source = parse_source(corrected_source, processed_source.path) + @team, _offenses = _investigate(processed_source) end - RuboCop::Markdown::Preprocess.restore!(new_source) - - assert_equal(correction, new_source) - ensure - FileUtils.rm_f(@processed_source.path) + assert_equal(source, new_source) end # rubocop:enable Metrics/AbcSize - def investigate(_cop, processed_source) - commissioner = RuboCop::Cop::Commissioner.new(registry.cops, registry.class.forces_for(registry.cops), raise_error: true) - commissioner.investigate(processed_source) - commissioner + def autocorrect_from_team(processed_source) + autocorrect = @team.instance_variable_get(:@options)[:stdin] + if autocorrect == true + # stdin wasn't modified. No autocorrect took place, return previous source + processed_source.buffer.source + else + autocorrect + end end - def _investigate(_cop, processed_source) - team = RuboCop::Cop::Team.new(registry.cops, configuration, raise_error: true, autocorrect: true) - report = team.investigate(processed_source) - @last_corrector = report.correctors.compact.first || RuboCop::Cop::Corrector.new(processed_source) - report.offenses + def assert_no_offenses(source, file = "test.md") + original_processed_source = parse_source(source, file) + _team, offenses = _investigate(original_processed_source) + + expected_annotations = RuboCop::RSpec::ExpectOffense::AnnotatedSource.parse(source) + actual_annotations = expected_annotations.with_offense_annotations(offenses) + assert_equal(expected_annotations, actual_annotations) end - def inspect_source(source, cop, file = "test.md") - super + def _investigate(original_processed_source) + # stdin: true will put the autocorrection in the stdin option for later use + team = RuboCop::Cop::Team.new(registry.cops, config, raise_error: true, autocorrect: true, stdin: true) + extracted_ruby_sources = Markdown::RubyExtractor.new(original_processed_source).call || [] + + investigations = extracted_ruby_sources.flat_map do |extracted_ruby_source| + team.investigate( + extracted_ruby_source[:processed_source], + offset: extracted_ruby_source[:offset], + original: original_processed_source + ) + end + [team, investigations.map(&:offenses).flatten] end - def parse_source!(source, file = "test.md") - super + def parse_source(source, file) + processed_source = RuboCop::ProcessedSource.new(source, ruby_version, file) + processed_source.config = config + processed_source.registry = registry + processed_source end def config - project_config_path = RuboCop::Markdown::PROJECT_ROOT.join(".rubocop.yml").to_s - project_config = RuboCop::ConfigLoader.load_file(project_config_path) - test_config = RuboCop::Config.new(project_config.merge(@config || {})) - RuboCop::ConfigLoader.merge_with_default(test_config, project_config_path) + @config ||= begin + project_config_path = RuboCop::Markdown::PROJECT_ROOT.join(".rubocop.yml").to_s + project_config = RuboCop::ConfigLoader.load_file(project_config_path) + test_config = RuboCop::Config.new(project_config.merge(@config || {})) + RuboCop::ConfigLoader.merge_with_default(test_config, project_config_path) + end + end + + def registry + @registry ||= begin + cops = config.keys.map { |cop| RuboCop::Cop::Registry.global.find_by_cop_name(cop) } + cops << cop_class if defined?(cop_class) && !cops.include?(cop_class) + cops.compact! + RuboCop::Cop::Registry.new(cops) + end + end + + def ruby_version + RuboCop::TargetRuby::DEFAULT_VERSION end end end diff --git a/test/offense_test.rb b/test/offense_test.rb index 23c1e8c..dfca26a 100644 --- a/test/offense_test.rb +++ b/test/offense_test.rb @@ -3,17 +3,6 @@ require "test_helper" class RuboCop::Markdown::OffenseTest < RuboCop::Markdown::Test - def overwrite_config - @old_store = RuboCop::Markdown.config_store - store = RuboCop::ConfigStore.new - store.instance_variable_set(:@options_config, config) - RuboCop::Markdown.config_store = store - end - - def teardown - RuboCop::Markdown.config_store = @old_store if @old_store - end - def test_single_snippet assert_offense(<<~MARKDOWN) # Before All @@ -176,7 +165,6 @@ def test_multiple_snippets end def test_multiple_invalid_snippets - skip("JRuby doesn't produce the second Lint/Syntax error") if RUBY_ENGINE == "jruby" assert_offense(<<~MARKDOWN) TestProf provides a built-in shared context for RSpec to profile examples individually: @@ -204,13 +192,11 @@ def test_multiple_invalid_snippets config.printer = :call_stack end ``` - ^{} Lint/Syntax: unexpected token $end MARKDOWN end def test_multiple_invalid_snippets_file_no_warn @config = { "Markdown" => { "WarnInvalid" => false } } - overwrite_config assert_no_offenses(<<~MARKDOWN) TestProf provides a built-in shared context for RSpec to profile examples individually: @@ -261,15 +247,27 @@ def test_non_code_offenses # rubocop:enable Layout/TrailingWhitespace def test_backticks_in_code - assert_offense(<<~MARKDOWN, marker: "##{RuboCop::Markdown::Preprocess::MARKER}") + assert_offense(<<~MARKDOWN) ```ruby `method_call + ^ Lint/Syntax: unterminated string meets end of file ``` - _{marker} ^ Lint/Syntax: unexpected token tXSTRING_BEG ```ruby further_code("", '') + ^^ Style/StringLiterals: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping. + ``` + MARKDOWN + + assert_correction(<<~MARKDOWN) + ```ruby + `method_call + ``` + + + ```ruby + further_code("", "") ``` MARKDOWN end diff --git a/test/rubocop_assertions.rb b/test/rubocop_assertions.rb deleted file mode 100644 index ebf841f..0000000 --- a/test/rubocop_assertions.rb +++ /dev/null @@ -1,152 +0,0 @@ -# frozen_string_literal: true - -# Extracted from https://github.com/rubocop/rubocop-minitest/blob/b446022ea09b3f5558df9c0106c9e714c6fc1ec5/lib/rubocop/minitest/assert_offense.rb -# rubocop:disable all - -require 'rubocop/rspec/expect_offense' -require 'rubocop/cop/legacy/corrector' - -module RuboCop - module Markdown - module AssertOffense - private - - def format_offense(source, **replacements) - replacements.each do |keyword, value| - value = value.to_s - source = source.gsub("%{#{keyword}}", value) - .gsub("^{#{keyword}}", '^' * value.size) - .gsub("_{#{keyword}}", ' ' * value.size) - end - source - end - - def assert_no_offenses(source, file = nil) - setup_assertion - - offenses = inspect_source(source, @cop, file) - - expected_annotations = RuboCop::RSpec::ExpectOffense::AnnotatedSource.parse(source) - actual_annotations = expected_annotations.with_offense_annotations(offenses) - - assert_equal(source, actual_annotations.to_s) - end - - def assert_offense(source, file = nil, **replacements) - setup_assertion - - @cop.instance_variable_get(:@options)[:autocorrect] = true - - source = format_offense(source, **replacements) - expected_annotations = RuboCop::RSpec::ExpectOffense::AnnotatedSource.parse(source) - if expected_annotations.plain_source == source - raise 'Use `assert_no_offenses` to assert that no offenses are found' - end - - @processed_source = parse_source!(expected_annotations.plain_source, file) - - offenses = _investigate(@cop, @processed_source) - - actual_annotations = expected_annotations.with_offense_annotations(offenses) - - assert_equal(expected_annotations.to_s, actual_annotations.to_s) - end - - def _investigate(cop, processed_source) - team = RuboCop::Cop::Team.new([cop], configuration, raise_error: true) - report = team.investigate(processed_source) - @last_corrector = report.correctors.first || RuboCop::Cop::Corrector.new(processed_source) - report.offenses - end - - def assert_correction(correction, loop: true) - raise '`assert_correction` must follow `assert_offense`' unless @processed_source - - iteration = 0 - new_source = loop do - iteration += 1 - - corrected_source = @last_corrector.rewrite - - break corrected_source unless loop - break corrected_source if @last_corrector.empty? || corrected_source == @processed_source.buffer.source - - if iteration > RuboCop::Runner::MAX_ITERATIONS - raise RuboCop::Runner::InfiniteCorrectionLoop.new(@processed_source.path, []) - end - - # Prepare for next loop - @processed_source = parse_source!(corrected_source, @processed_source.path) - - _investigate(@cop, @processed_source) - end - - assert_equal(correction, new_source) - end - - def setup_assertion - RuboCop::Formatter::DisabledConfigFormatter.config_to_allow_offenses = {} - RuboCop::Formatter::DisabledConfigFormatter.detected_styles = {} - end - - def inspect_source(source, cop, file = nil) - processed_source = parse_source!(source, file) - raise 'Error parsing example code' unless processed_source.valid_syntax? - - _investigate(cop, processed_source) - end - - def investigate(cop, processed_source) - needed = Hash.new { |h, k| h[k] = [] } - Array(cop.class.joining_forces).each { |force| needed[force] << cop } - forces = needed.map do |force_class, joining_cops| - force_class.new(joining_cops) - end - - commissioner = RuboCop::Cop::Commissioner.new([cop], forces, raise_error: true) - commissioner.investigate(processed_source) - commissioner - end - - def parse_source!(source, file = nil) - if file.respond_to?(:write) - file.write(source) - file.rewind - file = file.path - end - - processed_source = RuboCop::ProcessedSource.new(source, ruby_version, file) - - # Follow up https://github.com/rubocop/rubocop/pull/10987. - # When support for RuboCop 1.37.1 ends, this condition can be removed. - if processed_source.respond_to?(:config) && processed_source.respond_to?(:registry) - processed_source.config = configuration - processed_source.registry = registry - end - - processed_source - end - - def configuration - @configuration ||= if defined?(config) - config - else - RuboCop::Config.new({}, "#{Dir.pwd}/.rubocop.yml") - end - end - - def registry - @registry ||= begin - cops = configuration.keys.map { |cop| RuboCop::Cop::Registry.global.find_by_cop_name(cop) } - cops << cop_class if defined?(cop_class) && !cops.include?(cop_class) - cops.compact! - RuboCop::Cop::Registry.new(cops) - end - end - - def ruby_version - RuboCop::TargetRuby::DEFAULT_VERSION - end - end - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb index 1ee558f..7770495 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -11,6 +11,5 @@ require "minitest/autorun" require "rubocop" -require "rubocop_assertions" require "markdown_assertions" require "rubocop-md" From 184c000e9144b6cff1901e0e70c2c38f8c8d6c68 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Sat, 9 Mar 2024 12:54:49 +0100 Subject: [PATCH 4/5] Actually use `ruby_extractors` to make this all work --- config/default.yml | 21 +++++++++++++++++++ lib/rubocop/markdown.rb | 1 + test/fixtures/configs/config.yml | 4 ++++ test/fixtures/configs/config_with_require.yml | 4 ++++ test/fixtures/single_snippet.md | 6 ++++++ 5 files changed, 36 insertions(+) diff --git a/config/default.yml b/config/default.yml index 40dcaa1..ca1fd86 100644 --- a/config/default.yml +++ b/config/default.yml @@ -2,8 +2,29 @@ Markdown: WarnInvalid: true Autodetect: true +AllCops: + inherit_mode: + merge: + - Include + Include: + - '**/*.md' + - '**/*.livemd' + - '**/*.markdown' + - '**/*.mdown' + - '**/*.mdwn' + - '**/*.mdx' + - '**/*.mkd' + - '**/*.mkdn' + - '**/*.mkdown' + - '**/*.ronn' + - '**/*.scd' + - '**/*.workbook' + # Keep in sync with MARKDOWN_EXTENSIONS Layout/CommentIndentation: &markdown_excludes + inherit_mode: + merge: + - Exclude Exclude: - '**/*.md' - '**/*.livemd' diff --git a/lib/rubocop/markdown.rb b/lib/rubocop/markdown.rb index 295f4eb..d7bd770 100644 --- a/lib/rubocop/markdown.rb +++ b/lib/rubocop/markdown.rb @@ -14,5 +14,6 @@ module Markdown require_relative "markdown/ruby_extractor" RuboCop::Markdown::Inject.defaults! + RuboCop::Runner.ruby_extractors.unshift(RuboCop::Markdown::RubyExtractor) end end diff --git a/test/fixtures/configs/config.yml b/test/fixtures/configs/config.yml index f54004a..18c1ad9 100644 --- a/test/fixtures/configs/config.yml +++ b/test/fixtures/configs/config.yml @@ -1 +1,5 @@ inherit_from: "../../../.rubocop.yml" + +Lint/UnusedMethodArgument: + Exclude: + - "some_file.rb" diff --git a/test/fixtures/configs/config_with_require.yml b/test/fixtures/configs/config_with_require.yml index 81096ea..8de302c 100644 --- a/test/fixtures/configs/config_with_require.yml +++ b/test/fixtures/configs/config_with_require.yml @@ -2,3 +2,7 @@ inherit_from: "../../../.rubocop.yml" require: - "rubocop-md" + +Lint/UnusedMethodArgument: + Exclude: + - "some_file.rb" diff --git a/test/fixtures/single_snippet.md b/test/fixtures/single_snippet.md index bce4e71..931bc55 100644 --- a/test/fixtures/single_snippet.md +++ b/test/fixtures/single_snippet.md @@ -13,6 +13,12 @@ describe BeatleWeightedSearchQuery do @john = create(:beatle, name: 'John') end + # Lint/UnusedMethodArgument. The plugin config excludes this, + # verify that user configs with cop excludes inherit that + def do_something(foo) + puts "FOO" + end + # and about 15 examples here end ``` From 7c4feaafc5e31b6d91268ce09bba17de658ee002 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Sat, 9 Mar 2024 12:59:13 +0100 Subject: [PATCH 5/5] Support Prism with the new templating API This was previously happending automatically, but since we now create our own PorcessedSource this is necessary --- lib/rubocop/markdown/preprocess.rb | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/rubocop/markdown/preprocess.rb b/lib/rubocop/markdown/preprocess.rb index eefc68e..3eb7bfc 100644 --- a/lib/rubocop/markdown/preprocess.rb +++ b/lib/rubocop/markdown/preprocess.rb @@ -57,17 +57,30 @@ def call private + # rubocop:disable Metrics/MethodLength def new_processed_source(code, code_indent, original_processed_source) - processed_source = RuboCop::ProcessedSource.new( - strip_indent(code, code_indent), - original_processed_source.ruby_version, - original_processed_source.path - ) + # Can be dropped if RuboCop >= 1.62 only + supports_prism = original_processed_source.respond_to?(:parser_engine) + processed_source = if supports_prism + RuboCop::ProcessedSource.new( + strip_indent(code, code_indent), + original_processed_source.ruby_version, + original_processed_source.path, + parser_engine: original_processed_source.parser_engine + ) + else + RuboCop::ProcessedSource.new( + strip_indent(code, code_indent), + original_processed_source.ruby_version, + original_processed_source.path + ) + end processed_source.config = original_processed_source.config processed_source.registry = original_processed_source.registry processed_source end + # rubocop:enable Metrics/MethodLength # Strip indentation from code inside codeblocks def strip_indent(code, code_indent)