haml_lint 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/haml-lint +7 -0
- data/config/default.yml +91 -0
- data/lib/haml_lint/cli.rb +122 -0
- data/lib/haml_lint/configuration.rb +97 -0
- data/lib/haml_lint/configuration_loader.rb +68 -0
- data/lib/haml_lint/constants.rb +8 -0
- data/lib/haml_lint/exceptions.rb +15 -0
- data/lib/haml_lint/file_finder.rb +69 -0
- data/lib/haml_lint/haml_visitor.rb +36 -0
- data/lib/haml_lint/lint.rb +25 -0
- data/lib/haml_lint/linter/alt_text.rb +12 -0
- data/lib/haml_lint/linter/class_attribute_with_static_value.rb +51 -0
- data/lib/haml_lint/linter/classes_before_ids.rb +26 -0
- data/lib/haml_lint/linter/consecutive_comments.rb +20 -0
- data/lib/haml_lint/linter/consecutive_silent_scripts.rb +23 -0
- data/lib/haml_lint/linter/empty_script.rb +12 -0
- data/lib/haml_lint/linter/html_attributes.rb +14 -0
- data/lib/haml_lint/linter/implicit_div.rb +20 -0
- data/lib/haml_lint/linter/leading_comment_space.rb +14 -0
- data/lib/haml_lint/linter/line_length.rb +19 -0
- data/lib/haml_lint/linter/multiline_pipe.rb +43 -0
- data/lib/haml_lint/linter/multiline_script.rb +43 -0
- data/lib/haml_lint/linter/object_reference_attributes.rb +14 -0
- data/lib/haml_lint/linter/rubocop.rb +76 -0
- data/lib/haml_lint/linter/ruby_comments.rb +18 -0
- data/lib/haml_lint/linter/space_before_script.rb +52 -0
- data/lib/haml_lint/linter/space_inside_hash_attributes.rb +32 -0
- data/lib/haml_lint/linter/tag_name.rb +13 -0
- data/lib/haml_lint/linter/trailing_whitespace.rb +16 -0
- data/lib/haml_lint/linter/unnecessary_interpolation.rb +29 -0
- data/lib/haml_lint/linter/unnecessary_string_output.rb +39 -0
- data/lib/haml_lint/linter.rb +156 -0
- data/lib/haml_lint/linter_registry.rb +26 -0
- data/lib/haml_lint/logger.rb +107 -0
- data/lib/haml_lint/node_transformer.rb +28 -0
- data/lib/haml_lint/options.rb +89 -0
- data/lib/haml_lint/parser.rb +87 -0
- data/lib/haml_lint/rake_task.rb +107 -0
- data/lib/haml_lint/report.rb +16 -0
- data/lib/haml_lint/reporter/default_reporter.rb +39 -0
- data/lib/haml_lint/reporter/json_reporter.rb +44 -0
- data/lib/haml_lint/reporter.rb +36 -0
- data/lib/haml_lint/ruby_parser.rb +29 -0
- data/lib/haml_lint/runner.rb +76 -0
- data/lib/haml_lint/script_extractor.rb +181 -0
- data/lib/haml_lint/tree/comment_node.rb +5 -0
- data/lib/haml_lint/tree/doctype_node.rb +5 -0
- data/lib/haml_lint/tree/filter_node.rb +9 -0
- data/lib/haml_lint/tree/haml_comment_node.rb +18 -0
- data/lib/haml_lint/tree/node.rb +98 -0
- data/lib/haml_lint/tree/plain_node.rb +5 -0
- data/lib/haml_lint/tree/root_node.rb +5 -0
- data/lib/haml_lint/tree/script_node.rb +11 -0
- data/lib/haml_lint/tree/silent_script_node.rb +12 -0
- data/lib/haml_lint/tree/tag_node.rb +221 -0
- data/lib/haml_lint/utils.rb +58 -0
- data/lib/haml_lint/version.rb +4 -0
- data/lib/haml_lint.rb +36 -0
- metadata +175 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 343fc1d49926d0ac21d87fbfc26b93c518d690fc
|
4
|
+
data.tar.gz: 79d58c8fe3e0ded6c8cde5dee115276d84720fc1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 26259ff8ae9e582436736b4830a90c1256259a32d9d36372ec9dc8e823582c1cb4682bf2edb38c5db78b1d325c1d2baadd45f05fa8a1a3e6153aaa610bd38201
|
7
|
+
data.tar.gz: c30b1bcc9d65705bc944bd90e6743e2f26e35ebcb45df0158ad8db7702a7b03e7f94fe212265d91c88d780bbd112239bf88679f3e1e7614c585447f46d626468
|
data/bin/haml-lint
ADDED
data/config/default.yml
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# Default application configuration that all configurations inherit from.
|
2
|
+
#
|
3
|
+
# This is an opinionated list of which hooks are valuable to run and what their
|
4
|
+
# out of the box settings should be.
|
5
|
+
|
6
|
+
# Whether to ignore frontmatter at the beginning of HAML documents for
|
7
|
+
# frameworks such as Jekyll/Middleman
|
8
|
+
skip_frontmatter: false
|
9
|
+
|
10
|
+
linters:
|
11
|
+
AltText:
|
12
|
+
enabled: false
|
13
|
+
|
14
|
+
ClassAttributeWithStaticValue:
|
15
|
+
enabled: true
|
16
|
+
|
17
|
+
ClassesBeforeIds:
|
18
|
+
enabled: true
|
19
|
+
|
20
|
+
ConsecutiveComments:
|
21
|
+
enabled: true
|
22
|
+
|
23
|
+
ConsecutiveSilentScripts:
|
24
|
+
enabled: true
|
25
|
+
max_consecutive: 2
|
26
|
+
|
27
|
+
EmptyScript:
|
28
|
+
enabled: true
|
29
|
+
|
30
|
+
HtmlAttributes:
|
31
|
+
enabled: true
|
32
|
+
|
33
|
+
ImplicitDiv:
|
34
|
+
enabled: true
|
35
|
+
|
36
|
+
LeadingCommentSpace:
|
37
|
+
enabled: true
|
38
|
+
|
39
|
+
LineLength:
|
40
|
+
enabled: true
|
41
|
+
max: 80
|
42
|
+
|
43
|
+
MultilinePipe:
|
44
|
+
enabled: true
|
45
|
+
|
46
|
+
MultilineScript:
|
47
|
+
enabled: true
|
48
|
+
|
49
|
+
ObjectReferenceAttributes:
|
50
|
+
enabled: true
|
51
|
+
|
52
|
+
RuboCop:
|
53
|
+
enabled: true
|
54
|
+
# These cops are incredibly noisy when it comes to HAML templates, so we
|
55
|
+
# ignore them.
|
56
|
+
ignored_cops:
|
57
|
+
- Lint/BlockAlignment
|
58
|
+
- Lint/EndAlignment
|
59
|
+
- Lint/Void
|
60
|
+
- Metrics/LineLength
|
61
|
+
- Style/AlignParameters
|
62
|
+
- Style/BlockNesting
|
63
|
+
- Style/FileName
|
64
|
+
- Style/IfUnlessModifier
|
65
|
+
- Style/IndentationWidth
|
66
|
+
- Style/Next
|
67
|
+
- Style/TrailingBlankLines
|
68
|
+
- Style/TrailingWhitespace
|
69
|
+
- Style/WhileUntilModifier
|
70
|
+
|
71
|
+
RubyComments:
|
72
|
+
enabled: true
|
73
|
+
|
74
|
+
SpaceBeforeScript:
|
75
|
+
enabled: true
|
76
|
+
|
77
|
+
SpaceInsideHashAttributes:
|
78
|
+
enabled: true
|
79
|
+
style: space
|
80
|
+
|
81
|
+
TagName:
|
82
|
+
enabled: true
|
83
|
+
|
84
|
+
TrailingWhitespace:
|
85
|
+
enabled: true
|
86
|
+
|
87
|
+
UnnecessaryInterpolation:
|
88
|
+
enabled: true
|
89
|
+
|
90
|
+
UnnecessaryStringOutput:
|
91
|
+
enabled: true
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'haml_lint/options'
|
2
|
+
|
3
|
+
require 'sysexits'
|
4
|
+
|
5
|
+
module HamlLint
|
6
|
+
# Command line application interface.
|
7
|
+
class CLI
|
8
|
+
attr_accessor :options
|
9
|
+
|
10
|
+
# @param logger [HamlLint::Logger]
|
11
|
+
def initialize(logger)
|
12
|
+
@log = logger
|
13
|
+
end
|
14
|
+
|
15
|
+
# Parses the given command-line arguments and executes appropriate logic
|
16
|
+
# based on those arguments.
|
17
|
+
#
|
18
|
+
# @param args [Array<String>] command line arguments
|
19
|
+
# @return [Fixnum] exit status returned by the application
|
20
|
+
def run(args)
|
21
|
+
options = HamlLint::Options.new.parse(args)
|
22
|
+
act_on_options(options)
|
23
|
+
rescue => ex
|
24
|
+
handle_exception(ex)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :log
|
30
|
+
|
31
|
+
def act_on_options(options)
|
32
|
+
log.color_enabled = options.fetch(:color, log.tty?)
|
33
|
+
|
34
|
+
if options[:help]
|
35
|
+
print_help(options)
|
36
|
+
Sysexits::EX_OK
|
37
|
+
elsif options[:version]
|
38
|
+
print_version
|
39
|
+
Sysexits::EX_OK
|
40
|
+
elsif options[:show_linters]
|
41
|
+
print_available_linters
|
42
|
+
Sysexits::EX_OK
|
43
|
+
elsif options[:show_reporters]
|
44
|
+
print_available_reporters
|
45
|
+
Sysexits::EX_OK
|
46
|
+
else
|
47
|
+
scan_for_lints(options)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def handle_exception(ex)
|
52
|
+
case ex
|
53
|
+
when HamlLint::Exceptions::ConfigurationError
|
54
|
+
log.error ex.message
|
55
|
+
Sysexits::EX_CONFIG
|
56
|
+
when HamlLint::Exceptions::InvalidCLIOption
|
57
|
+
log.error ex.message
|
58
|
+
log.log "Run `#{APP_NAME}` --help for usage documentation"
|
59
|
+
Sysexits::EX_USAGE
|
60
|
+
when HamlLint::Exceptions::InvalidFilePath
|
61
|
+
log.error ex.message
|
62
|
+
Sysexits::EX_NOINPUT
|
63
|
+
when HamlLint::Exceptions::NoLintersError
|
64
|
+
log.error ex.message
|
65
|
+
Sysexits::EX_NOINPUT
|
66
|
+
else
|
67
|
+
print_unexpected_exception(ex)
|
68
|
+
Sysexits::EX_SOFTWARE
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def scan_for_lints(options)
|
73
|
+
report = Runner.new.run(options)
|
74
|
+
print_report(report, options)
|
75
|
+
report.failed? ? Sysexits::EX_DATAERR : Sysexits::EX_OK
|
76
|
+
end
|
77
|
+
|
78
|
+
def print_report(report, options)
|
79
|
+
reporter = options.fetch(:reporter, Reporter::DefaultReporter).new(log, report)
|
80
|
+
reporter.report_lints
|
81
|
+
end
|
82
|
+
|
83
|
+
def print_available_linters
|
84
|
+
log.info 'Available linters:'
|
85
|
+
|
86
|
+
linter_names = LinterRegistry.linters.map do |linter|
|
87
|
+
linter.name.split('::').last
|
88
|
+
end
|
89
|
+
|
90
|
+
linter_names.sort.each do |linter_name|
|
91
|
+
log.log " - #{linter_name}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def print_available_reporters
|
96
|
+
log.info 'Available reporters:'
|
97
|
+
|
98
|
+
reporter_names = Reporter.descendants.map do |reporter|
|
99
|
+
reporter.name.split('::').last.sub(/Reporter$/, '').downcase
|
100
|
+
end
|
101
|
+
|
102
|
+
reporter_names.sort.each do |reporter_name|
|
103
|
+
log.log " - #{reporter_name}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def print_help(options)
|
108
|
+
log.log options[:help]
|
109
|
+
end
|
110
|
+
|
111
|
+
def print_version
|
112
|
+
log.log "#{APP_NAME} #{HamlLint::VERSION}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def print_unexpected_exception(ex)
|
116
|
+
log.bold_error ex.message
|
117
|
+
log.error ex.backtrace.join("\n")
|
118
|
+
log.warning 'Report this bug at ', false
|
119
|
+
log.info HamlLint::BUG_REPORT_URL
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Stores configuration for haml-lint.
|
3
|
+
class Configuration
|
4
|
+
attr_reader :hash
|
5
|
+
|
6
|
+
# Creates a configuration from the given options hash.
|
7
|
+
#
|
8
|
+
# @param options [Hash]
|
9
|
+
def initialize(options)
|
10
|
+
@hash = options
|
11
|
+
validate
|
12
|
+
end
|
13
|
+
|
14
|
+
# Compares this configuration with another.
|
15
|
+
#
|
16
|
+
# @param other [HamlLint::Configuration]
|
17
|
+
# @return [true,false] whether the given configuration is equivalent
|
18
|
+
def ==(other)
|
19
|
+
super || @hash == other.hash
|
20
|
+
end
|
21
|
+
alias_method :eql?, :==
|
22
|
+
|
23
|
+
# Returns a non-modifiable configuration for the specified linter.
|
24
|
+
#
|
25
|
+
# @param linter [HamlLint::Linter,Class]
|
26
|
+
def for_linter(linter)
|
27
|
+
linter_name =
|
28
|
+
case linter
|
29
|
+
when Class
|
30
|
+
linter.name.split('::').last
|
31
|
+
when HamlLint::Linter
|
32
|
+
linter.name
|
33
|
+
else
|
34
|
+
linter.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
smart_merge(@hash['linters']['ALL'],
|
38
|
+
@hash['linters'].fetch(linter_name, {})).freeze
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns whether the specified linter is enabled by this configuration.
|
42
|
+
#
|
43
|
+
# @param linter [HamlLint::Linter,String]
|
44
|
+
def linter_enabled?(linter)
|
45
|
+
for_linter(linter)['enabled'] != false
|
46
|
+
end
|
47
|
+
|
48
|
+
# Merges the given configuration with this one, returning a new
|
49
|
+
# {Configuration}. The provided configuration will either add to or replace
|
50
|
+
# any options defined in this configuration.
|
51
|
+
#
|
52
|
+
# @param config [HamlLint::Configuration]
|
53
|
+
def merge(config)
|
54
|
+
self.class.new(smart_merge(@hash, config.hash))
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Validates the configuration for any invalid options, normalizing it where
|
60
|
+
# possible.
|
61
|
+
def validate
|
62
|
+
@hash = convert_nils_to_empty_hashes(@hash)
|
63
|
+
ensure_linter_section_exists(@hash)
|
64
|
+
end
|
65
|
+
|
66
|
+
def smart_merge(parent, child)
|
67
|
+
parent.merge(child) do |_key, old, new|
|
68
|
+
case old
|
69
|
+
when Array
|
70
|
+
old + Array(new)
|
71
|
+
when Hash
|
72
|
+
smart_merge(old, new)
|
73
|
+
else
|
74
|
+
new
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def ensure_linter_section_exists(hash)
|
80
|
+
hash['linters'] ||= {}
|
81
|
+
hash['linters']['ALL'] ||= {}
|
82
|
+
end
|
83
|
+
|
84
|
+
def convert_nils_to_empty_hashes(hash)
|
85
|
+
hash.each_with_object({}) do |(key, value), h|
|
86
|
+
h[key] =
|
87
|
+
case value
|
88
|
+
when nil then {}
|
89
|
+
when Hash then convert_nils_to_empty_hashes(value)
|
90
|
+
else
|
91
|
+
value
|
92
|
+
end
|
93
|
+
h
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module HamlLint
|
5
|
+
# Manages configuration file loading.
|
6
|
+
class ConfigurationLoader
|
7
|
+
DEFAULT_CONFIG_PATH = File.join(HAML_LINT_HOME, 'config', 'default.yml')
|
8
|
+
CONFIG_FILE_NAME = '.haml-lint.yml'
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def load_applicable_config
|
12
|
+
directory = File.expand_path(Dir.pwd)
|
13
|
+
config_file = possible_config_files(directory).find(&:file?)
|
14
|
+
|
15
|
+
if config_file
|
16
|
+
load_file(config_file.to_path)
|
17
|
+
else
|
18
|
+
default_configuration
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def default_configuration
|
23
|
+
@default_config ||= load_from_file(DEFAULT_CONFIG_PATH)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Loads a configuration, ensuring it extends the default configuration.
|
27
|
+
def load_file(file)
|
28
|
+
config = load_from_file(file)
|
29
|
+
|
30
|
+
default_configuration.merge(config)
|
31
|
+
rescue => error
|
32
|
+
raise HamlLint::Exceptions::ConfigurationError,
|
33
|
+
"Unable to load configuration from '#{file}': #{error}",
|
34
|
+
error.backtrace
|
35
|
+
end
|
36
|
+
|
37
|
+
def load_hash(hash)
|
38
|
+
config = HamlLint::Configuration.new(hash)
|
39
|
+
|
40
|
+
default_configuration.merge(config)
|
41
|
+
rescue => error
|
42
|
+
raise HamlLint::Exceptions::ConfigurationError,
|
43
|
+
"Unable to load configuration from '#{file}': #{error}",
|
44
|
+
error.backtrace
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def load_from_file(file)
|
50
|
+
hash =
|
51
|
+
if yaml = YAML.load_file(file)
|
52
|
+
yaml.to_hash
|
53
|
+
else
|
54
|
+
{}
|
55
|
+
end
|
56
|
+
|
57
|
+
HamlLint::Configuration.new(hash)
|
58
|
+
end
|
59
|
+
|
60
|
+
def possible_config_files(directory)
|
61
|
+
files = Pathname.new(directory)
|
62
|
+
.enum_for(:ascend)
|
63
|
+
.map { |path| path + CONFIG_FILE_NAME }
|
64
|
+
files << Pathname.new(CONFIG_FILE_NAME)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Collection of exceptions that can be raised by the HAML Lint application.
|
2
|
+
module HamlLint::Exceptions
|
3
|
+
# Raised when a {Configuration} could not be loaded from a file.
|
4
|
+
class ConfigurationError < StandardError; end
|
5
|
+
|
6
|
+
# Raised when invalid/incompatible command line options are provided.
|
7
|
+
class InvalidCLIOption < StandardError; end
|
8
|
+
|
9
|
+
# Raised when an invalid file path is specified
|
10
|
+
class InvalidFilePath < StandardError; end
|
11
|
+
|
12
|
+
# Raised when attempting to execute `Runner` with options that would result in
|
13
|
+
# no linters being enabled.
|
14
|
+
class NoLintersError < StandardError; end
|
15
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'find'
|
2
|
+
|
3
|
+
module HamlLint
|
4
|
+
# Finds HAML files that should be linted given a specified list of paths, glob
|
5
|
+
# patterns, and configuration.
|
6
|
+
class FileFinder
|
7
|
+
# List of extensions of files to include under a directory when a directory
|
8
|
+
# is specified instead of a file.
|
9
|
+
VALID_EXTENSIONS = %w[.haml]
|
10
|
+
|
11
|
+
# @param config [HamlLint::Configuration]
|
12
|
+
def initialize(config)
|
13
|
+
@config = config
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return list of files to lint given the specified set of paths and glob
|
17
|
+
# patterns.
|
18
|
+
# @param patterns [Array<String>]
|
19
|
+
# @param excluded_patterns [Array<String>]
|
20
|
+
# @raise [HamlLint::Exceptions::InvalidFilePath]
|
21
|
+
# @return [Array<String>] list of actual files
|
22
|
+
def find(patterns, excluded_patterns)
|
23
|
+
extract_files_from(patterns).reject do |file|
|
24
|
+
excluded_patterns.any? do |exclusion_glob|
|
25
|
+
::File.fnmatch?(exclusion_glob, file,
|
26
|
+
::File::FNM_PATHNAME | # Wildcards don't match path separators
|
27
|
+
::File::FNM_DOTMATCH) # `*` wildcard matches dotfiles
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def extract_files_from(patterns) # rubocop:disable MethodLength
|
35
|
+
files = []
|
36
|
+
|
37
|
+
patterns.each do |pattern|
|
38
|
+
if File.file?(pattern)
|
39
|
+
files << pattern
|
40
|
+
else
|
41
|
+
begin
|
42
|
+
::Find.find(pattern) do |file|
|
43
|
+
files << file if haml_file?(file)
|
44
|
+
end
|
45
|
+
rescue ::Errno::ENOENT
|
46
|
+
# File didn't exist; it might be a file glob pattern
|
47
|
+
matches = ::Dir.glob(pattern)
|
48
|
+
if matches.any?
|
49
|
+
files += matches
|
50
|
+
else
|
51
|
+
# One of the paths specified does not exist; raise a more
|
52
|
+
# descriptive exception so we know which one
|
53
|
+
raise HamlLint::Exceptions::InvalidFilePath,
|
54
|
+
"File path '#{pattern}' does not exist"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
files.uniq
|
61
|
+
end
|
62
|
+
|
63
|
+
def haml_file?(file)
|
64
|
+
return false unless ::FileTest.file?(file)
|
65
|
+
|
66
|
+
VALID_EXTENSIONS.include?(::File.extname(file))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Provides an interface which when included allows a class to visit nodes in
|
3
|
+
# the parse tree of a HAML document.
|
4
|
+
module HamlVisitor
|
5
|
+
def visit(node)
|
6
|
+
# Keep track of whether this block was consumed by the visitor. This
|
7
|
+
# allows us to visit all nodes by default, but can override the behavior
|
8
|
+
# by specifying `yield false` in a visit method, indicating that no
|
9
|
+
# further visiting should occur for the current node's children.
|
10
|
+
block_called = false
|
11
|
+
|
12
|
+
block = ->(descend = :children) do
|
13
|
+
block_called = true
|
14
|
+
visit_children(node) if descend == :children
|
15
|
+
end
|
16
|
+
|
17
|
+
method = "visit_#{node_name(node)}"
|
18
|
+
send(method, node, &block) if respond_to?(method, true)
|
19
|
+
|
20
|
+
# Visit all children by default unless the block was invoked (indicating
|
21
|
+
# the user intends to not recurse further, or wanted full control over
|
22
|
+
# when the children were visited).
|
23
|
+
visit_children(node) unless block_called
|
24
|
+
end
|
25
|
+
|
26
|
+
def visit_children(parent)
|
27
|
+
parent.children.each { |node| visit(node) }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def node_name(node)
|
33
|
+
node.type
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Contains information about a problem or issue with a HAML document.
|
3
|
+
class Lint
|
4
|
+
attr_reader :filename, :line, :linter, :message, :severity
|
5
|
+
|
6
|
+
# Creates a new lint.
|
7
|
+
#
|
8
|
+
# @param linter [HamlLint::Linter]
|
9
|
+
# @param filename [String]
|
10
|
+
# @param line [Fixnum]
|
11
|
+
# @param message [String]
|
12
|
+
# @param severity [Symbol]
|
13
|
+
def initialize(linter, filename, line, message, severity = :warning)
|
14
|
+
@linter = linter
|
15
|
+
@filename = filename
|
16
|
+
@line = line || 0
|
17
|
+
@message = message
|
18
|
+
@severity = severity
|
19
|
+
end
|
20
|
+
|
21
|
+
def error?
|
22
|
+
@severity == :error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for missing `alt` attributes on `img` tags.
|
3
|
+
class Linter::AltText < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_tag(node)
|
7
|
+
if node.tag_name == 'img' && !node.has_hash_attribute?(:alt)
|
8
|
+
add_lint(node, '`img` tags must include alt text')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for class attributes defined in tag attribute hash with static
|
3
|
+
# values.
|
4
|
+
#
|
5
|
+
# For example, it will prefer this:
|
6
|
+
#
|
7
|
+
# %tag.class-name
|
8
|
+
#
|
9
|
+
# ...over:
|
10
|
+
#
|
11
|
+
# %tag{ class: 'class-name' }
|
12
|
+
class Linter::ClassAttributeWithStaticValue < Linter
|
13
|
+
include LinterRegistry
|
14
|
+
|
15
|
+
STATIC_TYPES = [:str, :sym]
|
16
|
+
STATIC_CLASSES = [String, Symbol]
|
17
|
+
|
18
|
+
def visit_tag(node)
|
19
|
+
return unless contains_class_attribute?(node.dynamic_attributes_sources)
|
20
|
+
|
21
|
+
add_lint(node, 'Avoid defining `class` in attributes hash ' \
|
22
|
+
'for static class names')
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def contains_class_attribute?(attributes_sources)
|
28
|
+
attributes_sources.each do |code|
|
29
|
+
begin
|
30
|
+
ast_root = parse_ruby(code.start_with?('{') ? code : "{#{code}}")
|
31
|
+
rescue ::Parser::SyntaxError
|
32
|
+
next # RuboCop linter will report syntax errors
|
33
|
+
end
|
34
|
+
|
35
|
+
ast_root.children.each do |pair|
|
36
|
+
return true if static_class_attribute_value?(pair)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def static_class_attribute_value?(pair)
|
44
|
+
key, value = pair.children
|
45
|
+
|
46
|
+
STATIC_TYPES.include?(key.type) &&
|
47
|
+
key.children.first.to_sym == :class &&
|
48
|
+
STATIC_CLASSES.include?(value.children.first.class)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks that classes are listed before IDs in tags.
|
3
|
+
class Linter::ClassesBeforeIds < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
# Map of prefixes to the type of tag component
|
7
|
+
TYPES_BY_PREFIX = {
|
8
|
+
'.' => :class,
|
9
|
+
'#' => :id,
|
10
|
+
}
|
11
|
+
|
12
|
+
def visit_tag(node)
|
13
|
+
# Convert ".class#id" into [.class, #id] (preserving order)
|
14
|
+
components = node.static_attributes_source.scan(/[.#][^.#]+/)
|
15
|
+
|
16
|
+
(1...components.count).each do |index|
|
17
|
+
next unless components[index].start_with?('.') &&
|
18
|
+
components[index - 1].start_with?('#')
|
19
|
+
|
20
|
+
add_lint(node, 'Classes should be listed before IDs '\
|
21
|
+
"(#{components[index]} should precede #{components[index - 1]})")
|
22
|
+
break
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for multiple lines of code comments that can be condensed.
|
3
|
+
class Linter::ConsecutiveComments < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
MIN_CONSECUTIVE = 2
|
7
|
+
COMMENT_DETECTOR = ->(child) { child.type == :haml_comment }
|
8
|
+
|
9
|
+
def visit_root(node)
|
10
|
+
HamlLint::Utils.find_consecutive(
|
11
|
+
node.children,
|
12
|
+
MIN_CONSECUTIVE,
|
13
|
+
COMMENT_DETECTOR,
|
14
|
+
) do |group|
|
15
|
+
add_lint(group.first,
|
16
|
+
"#{group.count} consecutive comments can be merged into one")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for multiple consecutive silent script markers that could be
|
3
|
+
# condensed into a :ruby filter block.
|
4
|
+
class Linter::ConsecutiveSilentScripts < Linter
|
5
|
+
include LinterRegistry
|
6
|
+
|
7
|
+
SILENT_SCRIPT_DETECTOR = ->(child) do
|
8
|
+
child.type == :silent_script && child.children.empty?
|
9
|
+
end
|
10
|
+
|
11
|
+
def visit_root(node)
|
12
|
+
HamlLint::Utils.find_consecutive(
|
13
|
+
node.children,
|
14
|
+
config['max_consecutive'] + 1,
|
15
|
+
SILENT_SCRIPT_DETECTOR,
|
16
|
+
) do |group|
|
17
|
+
add_lint(group.first,
|
18
|
+
"#{group.count} consecutive Ruby scripts can be merged into " \
|
19
|
+
'a single `:ruby` filter')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|