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
17 changes: 12 additions & 5 deletions CHARTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,18 @@ already sitting in the `herb` gem's native C extension the whole time.
`herb-lint-rb` are the current names. If Marco's gem claims `Herb::Linter` or a
conflicting convention before herb-embedded sunsets, a rename may be needed in the
meantime — low stakes given the planned sunset, but worth a glance when his gem surfaces.
3. **Conformance punch list isn't closed.** The design spec's differential run against
`npx @herb-tools/linter` wasn't yet clean end-to-end — missing `LintContext` (filename),
missing `.herb.yml` config application, and per-rule `parserOptions` forwarding. Each has
a named cause and is tracked as implementation work in `bd` (see `herb-embedded-6dp` and
related beads), not a design gap.
3. **Conformance punch list, fixture-coverage dimension: closed.** The design spec's
differential run against `npx @herb-tools/linter` originally wasn't clean end-to-end —
missing `LintContext` (filename), missing `.herb.yml` config application, and per-rule
`parserOptions` forwarding. `LintContext` and `parserOptions` forwarding (including
`prism_program`/`prism_nodes`, see the Shape decisions entry above) are resolved; each had
a named cause tracked as implementation work in `bd` (`herb-embedded-6dp` and related
beads), not a design gap. `spec/conformance/conformance_spec.rb` now runs the differential
offense-for-offense diff against a hand-written fixture for every one of the 100 rules the
vendored bundle registers (`herb-embedded-ag7`), gated so an upstream rule with no fixture
fails the build — not just the 5 general-purpose fixtures from before. `.herb.yml` config
application is unaffected by that work and still worth a look if it's ever suspected of
drifting from upstream.
4. **Config schema stability.** `.herb.yml` config currently passes through to the engine
whole. Per-rule options can change across Herb releases, so Ruby may eventually need
version-aware translation instead of passthrough. Unresolved until it's actually observed
Expand Down
13 changes: 11 additions & 2 deletions js/ruby_backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ function __herbRegisterCustomRule(rewrittenSource, path) {
function __herbAutofix(source, file, ruleNames, includeUnsafe) {
var ruleClasses = __herbSelectRules(ruleNames);
var context = { fileName: file, filename: file };
var linter = new HerbLinter.Linter(__herbEmbeddedBridge.instance, ruleClasses);
var linter = new HerbLinter.Linter(__herbEmbeddedBridge.instance, ruleClasses, undefined, HerbLinter.rules);
var result = linter.autofix(source, context, undefined, { includeUnsafe: !!includeUnsafe });

return JSON.stringify({ source: result.source, fixed: result.fixed });
Expand All @@ -171,14 +171,23 @@ function __herbAutofix(source, file, ruleNames, includeUnsafe) {
// throws must not abort the run (spike 2 finding) — the real Linter#lint
// has no such isolation internally, since it runs every selected rule's
// check() in one uncaught loop.
//
// The 4th constructor arg (allAvailableRules) must be the full registry,
// not just the one rule being run: herb-disable-comment-unnecessary reads
// context.validRuleNames (built from Linter#getAvailableRules, which
// falls back to allAvailableRules) to decide whether a `herb:disable
// some-other-rule` comment references a real rule — omitting this arg
// left validRuleNames scoped to whatever single rule __herbLint happened
// to be running, so the rule silently never matched anything outside
// itself. Caught by conformance fixture coverage (herb-embedded-ag7).
function __herbLint(source, file, ruleNames) {
var ruleClasses = __herbSelectRules(ruleNames);
var context = { fileName: file, filename: file };
var offenses = [];

ruleClasses.forEach(function (ruleClass) {
try {
var linter = new HerbLinter.Linter(__herbEmbeddedBridge.instance, [ruleClass]);
var linter = new HerbLinter.Linter(__herbEmbeddedBridge.instance, [ruleClass], undefined, HerbLinter.rules);
var result = linter.lint(source, context);
offenses = offenses.concat(result.offenses);
} catch (e) {
Expand Down
64 changes: 58 additions & 6 deletions spec/conformance/conformance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
CONFORMANCE_ROOT = File.expand_path("../..", __dir__)
CONFORMANCE_REFERENCE_LINTER = File.join(CONFORMANCE_ROOT, "node_modules", ".bin", "herb-lint")
CONFORMANCE_FIXTURES = Dir.glob(File.join(__dir__, "fixtures", "*.html.erb"))
CONFORMANCE_RULE_FIXTURES = Dir.glob(File.join(__dir__, "fixtures", "rules", "*.html.erb"))

# A handful of rules (erb-no-instance-variables-in-partials and friends)
# only enable themselves for partial files (basename starting with `_`),
# so their fixture is named with that leading underscore to trigger
# correctly — this strips it back off to recover the rule code the
# coverage-gate test and --only/rules: need. Top-level (not an instance
# method) since it's needed both at spec-definition time, to build each
# `it` description, and at example-run time.
def conformance_rule_name_for(fixture_path)
File.basename(fixture_path, ".html.erb").sub(/\A_/, "")
end

# Node at test time is acceptable — the guarantee this repo makes is
# about *user* time (see spec/no_node_spec.rb). This is the differential
Expand All @@ -23,16 +35,24 @@

after { adapter.dispose }

def reference_offenses(fixture_path)
def reference_offenses(fixture_path, only: nil)
# --no-github: herb-lint auto-enables GitHub Actions annotations whenever
# GITHUB_ACTIONS=true is set (i.e. inside our own CI job), which then
# conflicts fatally with --json ("--github cannot be used with --json
# format"). Explicitly disabling it makes --json behavior independent of
# the invoking environment.
stdout, stderr, status = Open3.capture3(
#
# --only ignores .herb.yml config entirely (including a rule's own
# defaultConfig.enabled), matching rules: [...] on the bridge side —
# both let a per-rule fixture exercise a not-enabled-by-default rule
# directly, the same way gu7/ada's Bridge#lint specs already do.
args = [
CONFORMANCE_REFERENCE_LINTER, fixture_path, "--json", "--no-custom-rules", "--no-color", "--no-github",
"--jobs", "1", chdir: CONFORMANCE_ROOT
)
"--jobs", "1"
]
args += ["--only", only] if only

stdout, stderr, status = Open3.capture3(*args, chdir: CONFORMANCE_ROOT)
unless status.exitstatus.zero? || status.exitstatus == 1
raise "reference herb-lint failed (exit #{status.exitstatus}): #{stderr}"
end
Expand All @@ -44,9 +64,9 @@ def reference_offenses(fixture_path)
JSON.parse(stdout)["offenses"].map { |o| [o["code"], o["location"]["start"]["line"], o["location"]["start"]["column"]] }
end

def bridge_offenses(fixture_path)
def bridge_offenses(fixture_path, rules: nil)
source = File.read(fixture_path)
bridge.lint(source, file: File.basename(fixture_path)).map { |d| [d.rule, d.line, d.column] }
bridge.lint(source, file: File.basename(fixture_path), rules: rules).map { |d| [d.rule, d.line, d.column] }
end

CONFORMANCE_FIXTURES.each do |fixture_path|
Expand All @@ -71,4 +91,36 @@ def bridge_offenses(fixture_path)

expect(present).to include(*required)
end

CONFORMANCE_RULE_FIXTURES.each do |fixture_path|
rule = conformance_rule_name_for(fixture_path)

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
actual = bridge_offenses(fixture_path, rules: [rule]).sort

expect(reference).not_to be_empty,
"Fixture #{File.basename(fixture_path)} produced no reference offenses for " \
"#{rule} — it doesn't actually trigger the rule"

missing = reference - actual
extra = actual - reference

expect([missing, extra]).to eq([[], []]), <<~MSG
Conformance mismatch for #{rule} (#{File.basename(fixture_path)}) — [rule, line, column] tuples:
Missing (npx found it, bridge did not): #{missing.inspect}
Extra (bridge found it, npx did not): #{extra.inspect}
MSG
end
end

it "has a conformance fixture for every rule the vendored bundle registers" do
registered = bridge.rule_names
covered = CONFORMANCE_RULE_FIXTURES.map { |f| conformance_rule_name_for(f) }

missing = registered - covered
expect(missing).to be_empty,
"Rules registered by the vendored bundle with no conformance fixture under " \
"spec/conformance/fixtures/rules/: #{missing.sort.inspect}"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<p>Header</p>
<%# locals: (title:) %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= @widget_name %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>Hi</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<a href="/details">Click here</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div disabled>Unavailable</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<a href="/go"><button type="button">Go</button></a>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div accesskey="s">Save</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1 aria-label="Page Title">Welcome</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<meta aria-hidden="true">
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input type="text" autofocus>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<img src="cat.png" alt="a cat image">
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<svg><rect width="10" height="10"></rect></svg>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<% link_to "Home", "/" %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<% render "header" %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div <%= tag.attributes(class: "box", id: "main") %>></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= tag.hr "divider text" %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%# locals: (title:) %>

<p>Content</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<% # oops this should be an ERB comment %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<% case status %>
stray text
<% when :ok %>
fine
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%#= old_helper %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<% if show %>
<div>
<% end %>
content
<% if show %>
</div>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<% if active %>
<div class="a">
<% else %>
<div class="b">
<% end %>
content
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<% byebug %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<% if cond %>
<div>A</div>
<% else %>
<div>B</div>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<% if cond %>
<% end %>
1 change: 1 addition & 0 deletions spec/conformance/fixtures/rules/erb-no-empty-tags.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<% %>
5 changes: 5 additions & 0 deletions spec/conformance/fixtures/rules/erb-no-extra-newline.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<p>first</p>



<p>second</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= name %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<% case status; when :ok %>
fine
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="btn-<%= size %>"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= javascript_tag do %>
console.log("hi");
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= if cond %>
yes
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div <%= dynamic_attr %>="value"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div <%= extra_attrs %>></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div title="<%== @name %>"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<% track_view %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div aria-<% role %>-label="x"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script>
<% do_something %>
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<% if true then %>
yes
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<p>Hello
</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<button onclick="<%= handler %>">Click</button>
1 change: 1 addition & 0 deletions spec/conformance/fixtures/rules/erb-no-unsafe-raw.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div><%= raw(comment_body) %></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script>
var config = <%= settings %>;
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<% @user.name %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<% "unused" %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= "hello" %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<img src="<%= @photo %>" alt="pic">
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<p>Hi</p>

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%=x%>
1 change: 1 addition & 0 deletions spec/conformance/fixtures/rules/erb-right-trim.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<% x = 1 =%>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<% # locals: (name:) %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div><%# herb:disable foo, %></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div><%# herb:disable %></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div><%# herb:disable parser-no-errors, parser-no-errors %></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div><%# herb:disable all, parser-no-errors %></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div><%# herb:disable svg-tag-name-capitalization %></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div><%# herb:disable totally-not-a-real-rule %></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<script type="text/babel">console.log(1)</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<a>Learn more</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div aria-frobnicate="true">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div aria-label="myLabel">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div role="heading" aria-level="9">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div role="heading">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div role="fizzbuzz">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class='wrapper'>Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class ="wrapper">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class=wrapper>Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<button disabled aria-disabled="true">Save</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<html>
<head>
<p>Content</p>
</head>
<body></body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input disabled="disabled">
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<details>
<p>Content</p>
</details>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<html>
<head></head>
<body>
<base href="/">
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<iframe src="https://example.com"></iframe>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<img src="logo.png">
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input type="email">
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<nav>Content</nav>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div role="widget">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<body aria-hidden="true">
<p>Content</p>
</body>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<a href="/somewhere" aria-hidden="true">Go</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<span><div>Nested block</div></span>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="a" class="b">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<div id="widget">A</div>
<div id="widget">B</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<head>
<meta name="description" content="first">
<meta name="description" content="second">
</head>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1></h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<a href="/outer"><a href="/inner">Inner</a></a>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div tabindex="5">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="x">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div title="Tooltip">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div data_foo="bar">Content</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>1 < 2</p>
Loading
Loading