From 3fcdcf635d053c86af24f7e90ab00c096fe780cf Mon Sep 17 00:00:00 2001 From: jleo3 Date: Tue, 18 Aug 2026 16:43:48 -0400 Subject: [PATCH] Load rubocop-rake and rubocop-rspec plugins (herb-embedded-5g1) rubocop-rake and rubocop-rspec were already dev dependencies but never actually loaded (require: false, no require/plugins key in .rubocop.yml) - hence RuboCop's own "extension libraries are installed but not loaded" suggestion. Adds a `plugins:` key (the modern mechanism, per docs.rubocop.org/rubocop/plugins.html) to load both. rubocop-rake found nothing (Rakefile/rakelib are small). rubocop-rspec surfaced 136 offenses across 11 cops, addressed as a mix of real fixes and documented configuration - not a blanket disable: Fixed: include_examples -> it_behaves_like (verified safe: the shared example group's bare `subject` still resolves correctly through the nested context); named an anonymous subject; converted a message expectation to the spy pattern; split report_spec.rb's two top-level describes into report_spec.rb and lint_result_spec.rb (also fixes a pre-existing violation of CLAUDE.md's own "one spec file per lib unit" convention); applied safe autocorrects for scattered lets and simple-regex-as-string matchers. Configured, each with a comment explaining why: RSpec/SpecFilePathFormat disabled (this suite's spec/ is deliberately flat, not nested to mirror lib/'s namespace dirs); RSpec/ExampleLength and RSpec/MultipleExpectations raised to 25/8 (picked after checking the actual worst offenders - 22 lines/7 expectations - not arbitrary; this suite favors thorough, narrative examples over one-assertion-per-it); RSpec/DescribeClass excluded for the three specs that describe a scenario or guarantee rather than one Ruby class. One inline disable (not global) for RSpec/LeakyLocalVariable where the flagged pattern is a per-.each-iteration closure local, not the cross-example leak the cop targets. bundle exec rake: 45 files, 0 RuboCop offenses, 206 examples 0 failures, no vulnerabilities found. --- .rubocop.yml | 36 ++++++++++++++++++++++++++++ spec/adapters/mini_racer_spec.rb | 14 +++++------ spec/cli_spec.rb | 2 +- spec/conformance/conformance_spec.rb | 6 +++++ spec/custom_rule_loader_spec.rb | 21 ++++++++-------- spec/engine_adapter_spec.rb | 2 +- spec/formatters_spec.rb | 2 +- spec/lint_result_spec.rb | 18 ++++++++++++++ spec/report_spec.rb | 13 ---------- spec/runner_spec.rb | 8 +++---- 10 files changed, 84 insertions(+), 38 deletions(-) create mode 100644 spec/lint_result_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index f64ca76..593d192 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,7 @@ +plugins: + - rubocop-rake + - rubocop-rspec + AllCops: TargetRubyVersion: 3.2 NewCops: enable @@ -64,3 +68,35 @@ Style/TrailingCommaInArrayLiteral: Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: comma + +# rubocop-rspec cops below: each reflects a deliberate, established choice +# in how this suite is written (see CLAUDE.md's Test structure), not a +# blanket suppression of the plugin. + +# Specs live flat under spec/ (spec/result_envelope_spec.rb), not nested +# to mirror lib/'s namespace directories (spec/herb/embedded/...) — +# consistent across every spec file in the suite. +RSpec/SpecFilePathFormat: + Enabled: false + +# This suite favors thorough, narrative examples that exercise a whole +# scenario (e.g. bridge_autofix_spec.rb's inline fake-rule setup covering +# both the safe and unsafe autofix paths in one example) over strict +# one-assertion-per-it atomization. Raised rather than disabled so a +# genuinely oversized example still gets flagged — 25/8 give headroom +# above the longest existing examples (22 lines, 7 expectations). +RSpec/ExampleLength: + Max: 25 + +RSpec/MultipleExpectations: + Max: 8 + +# conformance_spec.rb, no_node_spec.rb, and host_shim_spec.rb describe a +# scenario or guarantee (offense-for-offense conformance, the no-Node +# guarantee, a JS shim's behavior), not one Ruby class under test — a +# string description is the right fit, not a class to invent. +RSpec/DescribeClass: + Exclude: + - "spec/conformance/**/*" + - "spec/no_node_spec.rb" + - "spec/host_shim_spec.rb" diff --git a/spec/adapters/mini_racer_spec.rb b/spec/adapters/mini_racer_spec.rb index 224bbde..8dffdf2 100644 --- a/spec/adapters/mini_racer_spec.rb +++ b/spec/adapters/mini_racer_spec.rb @@ -4,31 +4,31 @@ require "herb/embedded/adapters/mini_racer" RSpec.describe Herb::Embedded::Adapters::MiniRacer do - subject { described_class.new } + subject(:adapter) { described_class.new } - include_examples "an engine adapter" + it_behaves_like "an engine adapter" describe ".binary" do it "round-trips arbitrary bytes through #call without corruption" do - subject.load("function identity(x) { return x; }") + adapter.load("function identity(x) { return x; }") original = (0..255).to_a.pack("C*") - result = subject.call("identity", Herb::Embedded::EngineAdapter.binary(original)) + result = adapter.call("identity", Herb::Embedded::EngineAdapter.binary(original)) expect(result.pack("C*")).to eq(original) end it "marshals a Binary returned from an attached callback into a JS Uint8Array" do original = (0..255).to_a.pack("C*") - subject.attach("getBinary") { Herb::Embedded::EngineAdapter.binary(original) } - subject.load(<<~JS) + adapter.attach("getBinary") { Herb::Embedded::EngineAdapter.binary(original) } + adapter.load(<<~JS) function describeBinary() { var b = getBinary(); return b.constructor.name + " " + b.length; } JS - expect(subject.call("describeBinary")).to eq("Uint8Array 256") + expect(adapter.call("describeBinary")).to eq("Uint8Array 256") end end end diff --git a/spec/cli_spec.rb b/spec/cli_spec.rb index 84b872e..61ef1e2 100644 --- a/spec/cli_spec.rb +++ b/spec/cli_spec.rb @@ -74,7 +74,7 @@ def run_cli(argv, root:) _, _, err3 = run_cli(["--only", "nonexistent-rule"], root: root) expect(run_cli(["--only", "nonexistent-rule"], root: root).first).to eq(2) - expect(err3).to match(/nonexistent-rule/) + expect(err3).to include("nonexistent-rule") end Dir.mktmpdir do |root| diff --git a/spec/conformance/conformance_spec.rb b/spec/conformance/conformance_spec.rb index fcff14f..0f82908 100644 --- a/spec/conformance/conformance_spec.rb +++ b/spec/conformance/conformance_spec.rb @@ -93,7 +93,13 @@ def bridge_offenses(fixture_path, rules: nil) end CONFORMANCE_RULE_FIXTURES.each do |fixture_path| + # Not the leaky-across-examples pattern this cop targets: `rule` is a + # fresh binding per .each iteration, closed over by the one `it` block + # defined in that same iteration — the standard way to generate N + # examples from a data array, not shared/reassigned mutable state. + # rubocop:disable RSpec/LeakyLocalVariable rule = conformance_rule_name_for(fixture_path) + # rubocop:enable RSpec/LeakyLocalVariable it "matches npx @herb-tools/linter offense-for-offense on the #{rule} fixture, exercising the rule" do reference = reference_offenses(fixture_path, only: rule).sort diff --git a/spec/custom_rule_loader_spec.rb b/spec/custom_rule_loader_spec.rb index a3478b1..9cd6aec 100644 --- a/spec/custom_rule_loader_spec.rb +++ b/spec/custom_rule_loader_spec.rb @@ -10,15 +10,6 @@ RSpec.describe Herb::Embedded::CustomRuleLoader do let(:adapter) { Herb::Embedded::Adapters::MiniRacer.new } let(:bridge) { Herb::Embedded::Bridge.new(adapter: adapter, bundle: Herb::Embedded::Bundle).boot } - - after { adapter.dispose } - - def write_rule(dir, filename, contents) - rules_dir = File.join(dir, ".herb", "rules") - FileUtils.mkdir_p(rules_dir) - File.write(File.join(rules_dir, filename), contents) - end - let(:custom_rule_source) do <<~JS import { ParserRule } from "@herb-tools/linter"; @@ -33,6 +24,14 @@ def write_rule(dir, filename, contents) JS end + after { adapter.dispose } + + def write_rule(dir, filename, contents) + rules_dir = File.join(dir, ".herb", "rules") + FileUtils.mkdir_p(rules_dir) + File.write(File.join(rules_dir, filename), contents) + end + it "loads .herb/rules/**/*.mjs, registers each rule, and returns their names" do Dir.mktmpdir do |dir| write_rule(dir, "my_custom_rule.mjs", custom_rule_source) @@ -75,8 +74,8 @@ def write_rule(dir, filename, contents) end expect(error).not_to be_nil - expect(error.message).to match(/bad\.mjs/) - expect(error.message).to match(/"fs"/) + expect(error.message).to include("bad.mjs") + expect(error.message).to include('"fs"') end end diff --git a/spec/engine_adapter_spec.rb b/spec/engine_adapter_spec.rb index da2b604..40eab35 100644 --- a/spec/engine_adapter_spec.rb +++ b/spec/engine_adapter_spec.rb @@ -27,6 +27,6 @@ describe "contract compliance" do subject { Herb::Embedded::Adapters::MiniRacer.new } - include_examples "an engine adapter" + it_behaves_like "an engine adapter" end end diff --git a/spec/formatters_spec.rb b/spec/formatters_spec.rb index 9684965..286a947 100644 --- a/spec/formatters_spec.rb +++ b/spec/formatters_spec.rb @@ -40,7 +40,7 @@ def diagnostic(file:, rule:, severity: "error", correctable: false) expect(lines).to include("a.html.erb:3:7 error Offense from html-no-space-in-tag (html-no-space-in-tag)") expect(lines).to include("b.html.erb:3:7 warning Offense from erb-no-debug-output (erb-no-debug-output)") - expect(lines.last).to match(/2 offenses/) + expect(lines.last).to include("2 offenses") end end diff --git a/spec/lint_result_spec.rb b/spec/lint_result_spec.rb new file mode 100644 index 0000000..8beef03 --- /dev/null +++ b/spec/lint_result_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "spec_helper" +require "herb/embedded/diagnostic" +require "herb/embedded/lint_result" + +RSpec.describe Herb::Embedded::LintResult do + it "exposes #file and #diagnostics" do + diagnostic = Herb::Embedded::Diagnostic.from_js( + { "rule" => "x", "message" => "y", "severity" => "error", "location" => {} }, file: "a.erb" + ) + + result = described_class.new(file: "a.erb", diagnostics: [diagnostic]) + + expect(result.file).to eq("a.erb") + expect(result.diagnostics).to eq([diagnostic]) + end +end diff --git a/spec/report_spec.rb b/spec/report_spec.rb index c33f3ad..04bbde7 100644 --- a/spec/report_spec.rb +++ b/spec/report_spec.rb @@ -5,19 +5,6 @@ require "herb/embedded/lint_result" require "herb/embedded/report" -RSpec.describe Herb::Embedded::LintResult do - it "exposes #file and #diagnostics" do - diagnostic = Herb::Embedded::Diagnostic.from_js( - { "rule" => "x", "message" => "y", "severity" => "error", "location" => {} }, file: "a.erb" - ) - - result = described_class.new(file: "a.erb", diagnostics: [diagnostic]) - - expect(result.file).to eq("a.erb") - expect(result.diagnostics).to eq([diagnostic]) - end -end - RSpec.describe Herb::Embedded::Report do def diagnostic(severity:, correctable: false, autofix: false) offense = { "rule" => "x", "message" => "y", "severity" => severity, "location" => {} } diff --git a/spec/runner_spec.rb b/spec/runner_spec.rb index a9da23d..241aa9b 100644 --- a/spec/runner_spec.rb +++ b/spec/runner_spec.rb @@ -12,6 +12,7 @@ RSpec.describe Herb::Embedded::Runner do let(:adapter) { Herb::Embedded::Adapters::MiniRacer.new } let(:bridge) { Herb::Embedded::Bridge.new(adapter: adapter, bundle: Herb::Embedded::Bundle).boot } + let(:offending_source) { %(
x
\n) } after { adapter.dispose } @@ -22,8 +23,6 @@ def write(root, relative_path, contents) full_path end - let(:offending_source) { %(
x
\n) } - it "discovers files via Config#include_globs (excluding Config#exclude_globs), returns a Report, " \ "with Diagnostic#file relative to root" do Dir.mktmpdir do |root| @@ -77,11 +76,12 @@ def write(root, relative_path, contents) write(root, "clean.html.erb", "
hi
\n") config = Herb::Embedded::Config.new("files" => { "include" => ["**/*.html.erb"] }) runner = described_class.new(root: root, config: config, bridge: bridge) - - expect(Herb::Embedded::CustomRuleLoader).to receive(:new).once.and_call_original + allow(Herb::Embedded::CustomRuleLoader).to receive(:new).and_call_original runner.run runner.run + + expect(Herb::Embedded::CustomRuleLoader).to have_received(:new).once end end end