Skip to content

Commit

Permalink
Adds the StrictLocals linter following Rails' recommendations for u…
Browse files Browse the repository at this point in the history
…sing local variables in partials.

Close #486
  • Loading branch information
wilfison committed Feb 23, 2025
1 parent 198ebea commit d1132f8
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 0 deletions.
7 changes: 7 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ linters:
enabled: true
style: space

StrictLocals:
enabled: true
file_types: partials
matchers:
all: .*
partials: \A_.*\.haml\z

TagName:
enabled: true

Expand Down
45 changes: 45 additions & 0 deletions lib/haml_lint/linter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,51 @@ This offers the ability to ensure consistency of Haml hash
attributes style with ruby hash literal style (compare with
the Style/SpaceInsideHashLiteralBraces cop in Rubocop).

## StrictLocals

Checks for the presence of a `locals` magic comment at the beginning of a file.

**Bad:**
```haml
%h1= title
```

**Good**
```haml
-# locals: (title:)
%h1= title
```

If you want to disable the use of locals in partials, you can do it like this:

```haml
-# locals: ()
```

Option | Description
----------------|-------------------------------------------------------------
`file_types` | The class of files to lint (default `partial`)
`matchers` | The regular expressions to check file names against.

By default, this linter only runs on Rails-style partial views, e.g. files that
have a base name starting with a leading underscore `_`.

You can also define your own matchers if you want to enable this linter on
a different subset of your views. For instance, if you want to lint only files
starting with `special_`, you can define the configuration as follows:

```yaml
StrictLocals:
enabled: true
file_types: special
matchers:
special: ^special_.*\.haml$
```
Read more about the `locals` magic comment in the
[Ruby on Rails Guides](https://guides.rubyonrails.org/action_view_overview.html#strict-locals).

## TagName

Tag names should not contain uppercase letters.
Expand Down
58 changes: 58 additions & 0 deletions lib/haml_lint/linter/strict_locals.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

module HamlLint
# Checks for the presence of a `locals` magic comment at the beginning of a partial.
class Linter::StrictLocals < Linter
include LinterRegistry

DummyNode = Struct.new(:line)

# Enables the linter if the tree is for the right file type.
#
# @param [HamlLint::Tree::RootNode] the root of a syntax tree
# @return [true, false] whether the linter is enabled for the tree
def visit_root(root)
return unless enabled?(root)

first_children = root.children.first
return if first_children.is_a?(HamlLint::Tree::HamlCommentNode) &&
first_children.is_strict_locals?

record_lint(DummyNode.new(1), failure_message)
end

private

# Checks whether the linter is enabled for the file.
#
# @api private
# @return [true, false]
def enabled?(root)
matcher.match(File.basename(root.file)) ? true : false
end

# The type of files the linter is configured to check.
#
# @api private
# @return [String]
def file_types
@file_types ||= config['file_types'] || 'partial'
end

# The matcher to use for testing whether to check a file by file name.
#
# @api private
# @return [Regexp]
def matcher
@matcher ||= Regexp.new(config['matchers'][file_types] || '\A_.*\.haml\z')
end

# The error message when an `locals` comment is not found.
#
# @api private
# @return [String]
def failure_message
'Expected a strict `-# locals: ()` comment at the beginning of the file'
end
end
end
7 changes: 7 additions & 0 deletions lib/haml_lint/tree/haml_comment_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ def text
.rstrip
end

# Returns whether this comment contains a `locals` directive.
#
# @return [Boolean]
def is_strict_locals?
text.lstrip.start_with?('locals:')
end

private

def contained_directives
Expand Down
79 changes: 79 additions & 0 deletions spec/haml_lint/linter/strict_locals_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

RSpec.describe HamlLint::Linter::StrictLocals do
include_context 'linter'

context 'when the file name does not match the matcher' do
let(:haml) do
[
'%p= greeting',
'%p{ title: greeting }',
':ruby',
' x = greeting'
].join("\n")
end

it { should_not report_lint }
end

context 'when the file name matches the matcher' do
let(:options) do
{
config: HamlLint::ConfigurationLoader.default_configuration,
file: '_partial.html.haml'
}
end

context 'and there is a strict locals comment' do
let(:haml) do
[
'-# locals: (greeting:)',
'%p Hello, world'
].join("\n")
end

it { should_not report_lint }
end

context 'and there is no strict locals comment' do
let(:haml) { '%p Hello, world' }

it { should report_lint line: 1 }
end
end

context 'with a custom matcher' do
let(:haml) { '%p= @greeting' }
let(:full_config) do
HamlLint::Configuration.new(
'linters' => {
'StrictLocals' => {
'file_types' => 'my_custom',
'matchers' => {
'my_custom' => '\Apartial_.*\.haml\z'
}
}
}
)
end

let(:options) do
{
config: full_config,
file: file
}
end

context 'that matches the file name' do
let(:file) { 'partial_view.html.haml' }

it { should report_lint line: 1 }
end

context 'that does not match the file name' do
let(:file) { 'view.html.haml' }

it { should_not report_lint }
end
end
end

0 comments on commit d1132f8

Please sign in to comment.