Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add StrictLocals linter #553

Merged
merged 2 commits into from
Feb 24, 2025
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
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: false
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