haml_lint 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/bin/haml-lint +7 -0
  3. data/config/default.yml +91 -0
  4. data/lib/haml_lint/cli.rb +122 -0
  5. data/lib/haml_lint/configuration.rb +97 -0
  6. data/lib/haml_lint/configuration_loader.rb +68 -0
  7. data/lib/haml_lint/constants.rb +8 -0
  8. data/lib/haml_lint/exceptions.rb +15 -0
  9. data/lib/haml_lint/file_finder.rb +69 -0
  10. data/lib/haml_lint/haml_visitor.rb +36 -0
  11. data/lib/haml_lint/lint.rb +25 -0
  12. data/lib/haml_lint/linter/alt_text.rb +12 -0
  13. data/lib/haml_lint/linter/class_attribute_with_static_value.rb +51 -0
  14. data/lib/haml_lint/linter/classes_before_ids.rb +26 -0
  15. data/lib/haml_lint/linter/consecutive_comments.rb +20 -0
  16. data/lib/haml_lint/linter/consecutive_silent_scripts.rb +23 -0
  17. data/lib/haml_lint/linter/empty_script.rb +12 -0
  18. data/lib/haml_lint/linter/html_attributes.rb +14 -0
  19. data/lib/haml_lint/linter/implicit_div.rb +20 -0
  20. data/lib/haml_lint/linter/leading_comment_space.rb +14 -0
  21. data/lib/haml_lint/linter/line_length.rb +19 -0
  22. data/lib/haml_lint/linter/multiline_pipe.rb +43 -0
  23. data/lib/haml_lint/linter/multiline_script.rb +43 -0
  24. data/lib/haml_lint/linter/object_reference_attributes.rb +14 -0
  25. data/lib/haml_lint/linter/rubocop.rb +76 -0
  26. data/lib/haml_lint/linter/ruby_comments.rb +18 -0
  27. data/lib/haml_lint/linter/space_before_script.rb +52 -0
  28. data/lib/haml_lint/linter/space_inside_hash_attributes.rb +32 -0
  29. data/lib/haml_lint/linter/tag_name.rb +13 -0
  30. data/lib/haml_lint/linter/trailing_whitespace.rb +16 -0
  31. data/lib/haml_lint/linter/unnecessary_interpolation.rb +29 -0
  32. data/lib/haml_lint/linter/unnecessary_string_output.rb +39 -0
  33. data/lib/haml_lint/linter.rb +156 -0
  34. data/lib/haml_lint/linter_registry.rb +26 -0
  35. data/lib/haml_lint/logger.rb +107 -0
  36. data/lib/haml_lint/node_transformer.rb +28 -0
  37. data/lib/haml_lint/options.rb +89 -0
  38. data/lib/haml_lint/parser.rb +87 -0
  39. data/lib/haml_lint/rake_task.rb +107 -0
  40. data/lib/haml_lint/report.rb +16 -0
  41. data/lib/haml_lint/reporter/default_reporter.rb +39 -0
  42. data/lib/haml_lint/reporter/json_reporter.rb +44 -0
  43. data/lib/haml_lint/reporter.rb +36 -0
  44. data/lib/haml_lint/ruby_parser.rb +29 -0
  45. data/lib/haml_lint/runner.rb +76 -0
  46. data/lib/haml_lint/script_extractor.rb +181 -0
  47. data/lib/haml_lint/tree/comment_node.rb +5 -0
  48. data/lib/haml_lint/tree/doctype_node.rb +5 -0
  49. data/lib/haml_lint/tree/filter_node.rb +9 -0
  50. data/lib/haml_lint/tree/haml_comment_node.rb +18 -0
  51. data/lib/haml_lint/tree/node.rb +98 -0
  52. data/lib/haml_lint/tree/plain_node.rb +5 -0
  53. data/lib/haml_lint/tree/root_node.rb +5 -0
  54. data/lib/haml_lint/tree/script_node.rb +11 -0
  55. data/lib/haml_lint/tree/silent_script_node.rb +12 -0
  56. data/lib/haml_lint/tree/tag_node.rb +221 -0
  57. data/lib/haml_lint/utils.rb +58 -0
  58. data/lib/haml_lint/version.rb +4 -0
  59. data/lib/haml_lint.rb +36 -0
  60. 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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'haml_lint'
4
+ require 'haml_lint/cli'
5
+
6
+ logger = HamlLint::Logger.new(STDOUT)
7
+ exit HamlLint::CLI.new(logger).run(ARGV)
@@ -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,8 @@
1
+ # Global application constants.
2
+ module HamlLint
3
+ HAML_LINT_HOME = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
4
+ APP_NAME = 'haml-lint'
5
+
6
+ REPO_URL = 'https://github.com/brigade/haml-lint'
7
+ BUG_REPORT_URL = "#{REPO_URL}/issues"
8
+ 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