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.
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