Skip to content
Draft
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
1 change: 1 addition & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ rubygem:
- changed-files:
- any-glob-to-any-file:
- 'herb.gemspec'
- 'gems/**/*'
- 'lib/**/*'
- 'templates/lib/**/*'

Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/build-gems.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
paths:
- "src/**"
- "lib/**"
- "gems/**"
- "ext/**"
- "Gemfile"
- "Gemfile.lock"
Expand All @@ -22,6 +23,7 @@ on:
paths:
- "src/**"
- "lib/**"
- "gems/**"
- "ext/**"
- "Gemfile"
- "Gemfile.lock"
Expand Down Expand Up @@ -89,6 +91,9 @@ jobs:
run: |
if [ "${{ matrix.target }}" = "default" ]; then
bundle exec rake build
mkdir -p pkg
cd gems/rubocop-herb
gem build rubocop-herb.gemspec --output ../../pkg/rubocop-herb.gem
else
bundle exec rake gem:${{ matrix.target }}
fi
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
run: bundle exec rake compile

- name: Run Ruby Tests
run: bundle exec rake test:all
run: bundle exec rake test:all test:rubocop_herb

- name: Run C tests
run: ./run_herb_tests
Expand Down
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ Metrics/ClassLength:
- lib/herb/visitor/stack.rb
- templates/template.rb
- test/**/*_test.rb
- gems/**/test/**/*_test.rb

Metrics/ModuleLength:
Exclude:
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ source "https://rubygems.org"

gemspec

gem "rubocop-herb", path: "gems/rubocop-herb"

gem "prism", github: "ruby/prism", tag: "v1.9.0"

gem "actionview", "~> 8.1", require: "action_view"
Expand Down
9 changes: 9 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ PATH
specs:
herb (0.11.0)

PATH
remote: gems/rubocop-herb
specs:
rubocop-herb (0.11.0)
herb (= 0.11.0)
lint_roller (~> 1.1)
rubocop (>= 1.72.1, < 2)

GEM
remote: https://rubygems.org/
specs:
Expand Down Expand Up @@ -368,6 +376,7 @@ DEPENDENCIES
reline (~> 0.7)
rolldown (~> 1.2)
rubocop (~> 1.72)
rubocop-herb!
rubocop-performance (~> 1.26.0)
sorbet
steep (~> 2.1)
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ Run the linter and the formatter:

```sh
npx @herb-tools/linter
```

Run your project's configured RuboCop rules against Ruby in `.erb` and `.herb` templates with the `rubocop-herb` plugin:

```sh
bundle add rubocop-herb
```

```yaml
# .rubocop.yml
plugins:
- rubocop-herb
```

```sh
bundle exec rubocop app/views
```

Format your HTML+ERB templates:

```sh
npx @herb-tools/formatter
```

Expand Down
6 changes: 6 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ Rake::TestTask.new("test:all") do |t|
t.test_files = FileList["test/**/*_test.rb"]
end

Rake::TestTask.new("test:rubocop_herb") do |t|
t.libs << "gems/rubocop-herb/test"
t.libs << "gems/rubocop-herb/lib"
t.test_files = FileList["gems/rubocop-herb/test/**/*_test.rb"]
end

task "make" do
puts "Running make..."
IO.popen("make") do |output|
Expand Down
26 changes: 26 additions & 0 deletions gems/rubocop-herb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# rubocop-herb

`rubocop-herb` runs a project's configured RuboCop rules against Ruby embedded in `.erb` and `.herb` templates. Herb builds a position-preserving Ruby representation of the entire template so RuboCop can understand control flow and variable usage across ERB tags while reporting offenses at their original template locations.

Add the gem to your bundle:

```sh
bundle add rubocop-herb
```

Then enable the plugin in `.rubocop.yml`:

```yaml
plugins:
- rubocop-herb
```

Then use RuboCop normally:

```sh
bundle exec rubocop app/views
bundle exec rubocop -a app/views
bundle exec rubocop -A app/views
```

The plugin honors the project's RuboCop configuration, including custom cops, inherited configuration, target Ruby versions, file exclusions, and autocorrection. Cops that depend on physical Ruby file contents or layout are [excluded for template files by default](config/default.yml).
108 changes: 108 additions & 0 deletions gems/rubocop-herb/config/default.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
inherit_mode:
merge:
- Exclude
- Include

AllCops:
Include: &template_files
- '**/*.erb'
- '**/*.herb'

# These cops depend on physical Ruby file contents or layout that is obscured
# by the position-preserving template representation.
Layout/ArgumentAlignment:
Exclude: *template_files
Layout/ArrayAlignment:
Exclude: *template_files
Layout/BlockAlignment:
Exclude: *template_files
Layout/ClosingParenthesisIndentation:
Exclude: *template_files
Layout/CommentIndentation:
Exclude: *template_files
Layout/InitialIndentation:
Exclude: *template_files
Layout/EmptyLineAfterGuardClause:
Exclude: *template_files
Layout/EndAlignment:
Exclude: *template_files
Layout/FirstArgumentIndentation:
Exclude: *template_files
Layout/FirstArrayElementIndentation:
Exclude: *template_files
Layout/FirstArrayElementLineBreak:
Exclude: *template_files
Layout/FirstHashElementIndentation:
Exclude: *template_files
Layout/FirstHashElementLineBreak:
Exclude: *template_files
Layout/FirstMethodArgumentLineBreak:
Exclude: *template_files
Layout/FirstParameterIndentation:
Exclude: *template_files
Layout/HashAlignment:
Exclude: *template_files
Layout/IndentationConsistency:
Exclude: *template_files
Layout/IndentationWidth:
Exclude: *template_files
Layout/LeadingEmptyLine:
Exclude: *template_files
Layout/LeadingEmptyLines:
Exclude: *template_files
Layout/LineEndStringConcatenationIndentation:
Exclude: *template_files
Layout/LineLength:
Exclude: *template_files
Layout/MultilineArrayBraceLayout:
Exclude: *template_files
Layout/MultilineAssignmentLayout:
Exclude: *template_files
Layout/MultilineHashBraceLayout:
Exclude: *template_files
Layout/MultilineMethodCallBraceLayout:
Exclude: *template_files
Layout/MultilineMethodCallIndentation:
Exclude: *template_files
Layout/MultilineMethodDefinitionBraceLayout:
Exclude: *template_files
Layout/MultilineOperationIndentation:
Exclude: *template_files
Layout/ParameterAlignment:
Exclude: *template_files
Layout/SpaceBeforeSemicolon:
Exclude: *template_files
Layout/TrailingEmptyLines:
Exclude: *template_files
Layout/TrailingWhitespace:
Exclude: *template_files
Lint/EmptyFile:
Exclude: *template_files
Lint/EmptyConditionalBody:
Exclude: *template_files
Metrics/BlockLength:
Exclude: *template_files
Naming/FileName:
Exclude: *template_files
Rails/Presence:
Exclude: *template_files
Style/FrozenStringLiteralComment:
Exclude: *template_files
Style/EmptyElse:
Exclude: *template_files
Style/IdenticalConditionalBranches:
Exclude: *template_files
Style/IfWithSemicolon:
Exclude: *template_files
Style/IfUnlessModifier:
Exclude: *template_files
Style/MultilineTernaryOperator:
Exclude: *template_files
Style/NestedTernaryOperator:
Exclude: *template_files
Style/Next:
Exclude: *template_files
Style/Semicolon:
Exclude: *template_files
Style/WhileUntilModifier:
Exclude: *template_files
6 changes: 6 additions & 0 deletions gems/rubocop-herb/lib/rubocop/herb.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

require "herb"
require "rubocop"

require_relative "herb/plugin"
74 changes: 74 additions & 0 deletions gems/rubocop-herb/lib/rubocop/herb/plugin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

require "lint_roller"

module RuboCop
module Herb
class Plugin < LintRoller::Plugin
def about
LintRoller::About.new(
description: "Run configured RuboCop rules against Ruby in ERB templates.",
homepage: "https://github.com/marcoroth/herb",
name: "rubocop-herb",
version: ::Herb::VERSION
)
end

def rules(_context)
RuboCop::Runner.ruby_extractors.unshift(EXTRACT_RUBY)

LintRoller::Rules.new(
config_format: :rubocop,
type: :path,
value: File.expand_path("../../../config/default.yml", __dir__)
)
end

def supported?(context)
context.engine == :rubocop
end

def self.extract_ruby(processed_source)
return unless processed_source.path&.end_with?(".erb", ".herb")

template = processed_source.raw_source
code = position_preserving_ruby(template)
return [] if code.strip.empty?

source = build_processed_source(code, original: processed_source)
return [] if source.valid_syntax? && !source.ast

[{ offset: 0, processed_source: source }]
end

def self.build_processed_source(code, original:)
::RuboCop::ProcessedSource.new(
code,
original.ruby_version,
original.path,
parser_engine: original.parser_engine
).tap do |source|
source.config = original.config
source.registry = original.registry
end
end

def self.position_preserving_ruby(template)
extracted = ::Herb.extract_ruby(template)
return extracted if extracted.length == template.length

byte_offset = 0
template.each_char.map do |character|
extracted_character = extracted.byteslice(byte_offset, character.bytesize)
byte_offset += character.bytesize

extracted_character == character ? character : extracted_character.each_char.first
end.join
end

private_class_method :build_processed_source, :position_preserving_ruby

EXTRACT_RUBY = method(:extract_ruby)
end
end
end
34 changes: 34 additions & 0 deletions gems/rubocop-herb/rubocop-herb.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

require_relative "../../lib/herb/version"

Gem::Specification.new do |spec|
spec.name = "rubocop-herb"
spec.version = Herb::VERSION
spec.authors = ["Marco Roth"]
spec.email = ["marco.roth@intergga.ch"]

spec.summary = "Run RuboCop against Ruby embedded in Herb templates"
spec.description = "A RuboCop plugin that uses Herb to extract Ruby from ERB templates."
spec.homepage = "https://herb-tools.dev"
spec.license = "MIT"

spec.required_ruby_version = ">= 3.2.0"
spec.require_paths = ["lib"]
spec.files = Dir[
"README.md",
"config/**/*.yml",
"lib/**/*.rb"
]

spec.metadata["allowed_push_host"] = "https://rubygems.org"
spec.metadata["rubygems_mfa_required"] = "true"
spec.metadata["default_lint_roller_plugin"] = "RuboCop::Herb::Plugin"
spec.metadata["homepage_uri"] = "https://herb-tools.dev"
spec.metadata["source_code_uri"] = "https://github.com/marcoroth/herb"
spec.metadata["bug_tracker_uri"] = "https://github.com/marcoroth/herb/issues"

spec.add_dependency "herb", "= #{Herb::VERSION}"
spec.add_dependency "lint_roller", "~> 1.1"
spec.add_dependency "rubocop", ">= 1.72.1", "< 2"
end
Loading
Loading