Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
plugins:
- rubocop-rake
- rubocop-rspec

AllCops:
TargetRubyVersion: 3.2
NewCops: enable
Expand Down Expand Up @@ -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"
14 changes: 7 additions & 7 deletions spec/adapters/mini_racer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion spec/cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
6 changes: 6 additions & 0 deletions spec/conformance/conformance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 10 additions & 11 deletions spec/custom_rule_loader_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion spec/engine_adapter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion spec/formatters_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 18 additions & 0 deletions spec/lint_result_spec.rb
Original file line number Diff line number Diff line change
@@ -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
13 changes: 0 additions & 13 deletions spec/report_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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" => {} }
Expand Down
8 changes: 4 additions & 4 deletions spec/runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) { %(<div class="a">x</div>\n) }

after { adapter.dispose }

Expand All @@ -22,8 +23,6 @@ def write(root, relative_path, contents)
full_path
end

let(:offending_source) { %(<div class="a">x</div>\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|
Expand Down Expand Up @@ -77,11 +76,12 @@ def write(root, relative_path, contents)
write(root, "clean.html.erb", "<div>hi</div>\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
Loading