slim_lint 0.2.0 → 0.3.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/bin/slim-lint +0 -1
  3. data/lib/slim_lint/atom.rb +91 -0
  4. data/lib/slim_lint/capture_map.rb +17 -0
  5. data/lib/slim_lint/cli.rb +25 -8
  6. data/lib/slim_lint/configuration.rb +38 -31
  7. data/lib/slim_lint/configuration_loader.rb +21 -5
  8. data/lib/slim_lint/document.rb +20 -4
  9. data/lib/slim_lint/engine.rb +6 -0
  10. data/lib/slim_lint/file_finder.rb +13 -4
  11. data/lib/slim_lint/filters/inject_line_numbers.rb +7 -2
  12. data/lib/slim_lint/filters/sexp_converter.rb +4 -0
  13. data/lib/slim_lint/lint.rb +17 -1
  14. data/lib/slim_lint/linter/comment_control_statement.rb +2 -2
  15. data/lib/slim_lint/linter/consecutive_control_statements.rb +1 -16
  16. data/lib/slim_lint/linter/empty_control_statement.rb +1 -1
  17. data/lib/slim_lint/linter/redundant_div.rb +8 -5
  18. data/lib/slim_lint/linter/rubocop.rb +40 -18
  19. data/lib/slim_lint/linter/tag_case.rb +1 -1
  20. data/lib/slim_lint/linter.rb +19 -6
  21. data/lib/slim_lint/linter_registry.rb +13 -2
  22. data/lib/slim_lint/linter_selector.rb +74 -0
  23. data/lib/slim_lint/logger.rb +5 -9
  24. data/lib/slim_lint/matcher/anything.rb +9 -0
  25. data/lib/slim_lint/matcher/base.rb +19 -0
  26. data/lib/slim_lint/matcher/capture.rb +30 -0
  27. data/lib/slim_lint/matcher/nothing.rb +11 -0
  28. data/lib/slim_lint/options.rb +16 -6
  29. data/lib/slim_lint/rake_task.rb +15 -0
  30. data/lib/slim_lint/report.rb +7 -0
  31. data/lib/slim_lint/reporter/default_reporter.rb +2 -2
  32. data/lib/slim_lint/reporter/json_reporter.rb +15 -10
  33. data/lib/slim_lint/reporter.rb +17 -11
  34. data/lib/slim_lint/ruby_extractor.rb +2 -0
  35. data/lib/slim_lint/runner.rb +43 -39
  36. data/lib/slim_lint/sexp.rb +43 -30
  37. data/lib/slim_lint/sexp_visitor.rb +66 -28
  38. data/lib/slim_lint/utils.rb +17 -0
  39. data/lib/slim_lint/version.rb +1 -1
  40. data/lib/slim_lint.rb +10 -1
  41. metadata +10 -3
@@ -1,72 +1,76 @@
1
1
  module SlimLint
2
2
  # Responsible for running the applicable linters against the desired files.
3
3
  class Runner
4
- # Make the list of applicable files available
5
- attr_reader :files
6
-
7
4
  # Runs the appropriate linters against the desired files given the specified
8
5
  # options.
9
6
  #
10
7
  # @param options [Hash]
11
- # @raise [SlimLint::Exceptions::NoLintersError] when no linters are enabled
8
+ # @option config_file [String] path of configuration file to load
9
+ # @option config [SlimLint::Configuration] configuration to use
10
+ # @option excluded_files [Array<String>]
11
+ # @option included_linters [Array<String>]
12
+ # @option excluded_linters [Array<String>]
12
13
  # @return [SlimLint::Report] a summary of all lints found
13
14
  def run(options = {})
14
15
  config = load_applicable_config(options)
15
- files = extract_applicable_files(options, config)
16
- linters = extract_enabled_linters(config, options)
16
+ files = extract_applicable_files(config, options)
17
17
 
18
- raise SlimLint::Exceptions::NoLintersError, 'No linters specified' if linters.empty?
18
+ linter_selector = SlimLint::LinterSelector.new(config, options)
19
19
 
20
- @lints = []
21
- files.each do |file|
22
- find_lints(file, linters, config)
23
- end
20
+ lints = files.map do |file|
21
+ collect_lints(file, linter_selector, config)
22
+ end.flatten
24
23
 
25
- SlimLint::Report.new(@lints, files)
24
+ SlimLint::Report.new(lints, files)
26
25
  end
27
26
 
28
27
  private
29
28
 
29
+ # Returns the {SlimLint::Configuration} that should be used given the
30
+ # specified options.
31
+ #
32
+ # @param options [Hash]
33
+ # @return [SlimLint::Configuration]
30
34
  def load_applicable_config(options)
31
35
  if options[:config_file]
32
36
  SlimLint::ConfigurationLoader.load_file(options[:config_file])
37
+ elsif options[:config]
38
+ options[:config]
33
39
  else
34
40
  SlimLint::ConfigurationLoader.load_applicable_config
35
41
  end
36
42
  end
37
43
 
38
- def extract_enabled_linters(config, options)
39
- included_linters = LinterRegistry
40
- .extract_linters_from(options.fetch(:included_linters, []))
41
-
42
- included_linters = LinterRegistry.linters if included_linters.empty?
43
-
44
- excluded_linters = LinterRegistry
45
- .extract_linters_from(options.fetch(:excluded_linters, []))
46
-
47
- # After filtering out explicitly included/excluded linters, only include
48
- # linters which are enabled in the configuration
49
- (included_linters - excluded_linters).map do |linter_class|
50
- linter_config = config.for_linter(linter_class)
51
- linter_class.new(linter_config) if linter_config['enabled']
52
- end.compact
53
- end
54
-
55
- def find_lints(file, linters, config)
56
- document = SlimLint::Document.new(File.read(file), file: file, config: config)
57
-
58
- linters.each do |linter|
59
- @lints += linter.run(document)
44
+ # Runs all provided linters using the specified config against the given
45
+ # file.
46
+ #
47
+ # @param file [String] path to file to lint
48
+ # @param linter_selector [SlimLint::LinterSelector]
49
+ # @param config [SlimLint::Configuration]
50
+ def collect_lints(file, linter_selector, config)
51
+ begin
52
+ document = SlimLint::Document.new(File.read(file), file: file, config: config)
53
+ rescue Slim::Parser::SyntaxError => ex
54
+ return [SlimLint::Lint.new(nil, file, ex.line, ex.error, :error)]
60
55
  end
61
- rescue Slim::Parser::SyntaxError => ex
62
- @lints << Lint.new(nil, file, ex.line, ex.error, :error)
56
+
57
+ linter_selector.linters_for_file(file).map do |linter|
58
+ linter.run(document)
59
+ end.flatten
63
60
  end
64
61
 
65
- def extract_applicable_files(options, config)
62
+ # Returns the list of files that should be linted given the specified
63
+ # configuration and options.
64
+ #
65
+ # @param config [SlimLint::Configuration]
66
+ # @param options [Hash]
67
+ # @return [Array<String>]
68
+ def extract_applicable_files(config, options)
66
69
  included_patterns = options[:files]
67
- excluded_files = options.fetch(:excluded_files, [])
70
+ excluded_patterns = config['exclude']
71
+ excluded_patterns += options.fetch(:excluded_files, [])
68
72
 
69
- SlimLint::FileFinder.new(config).find(included_patterns, excluded_files)
73
+ SlimLint::FileFinder.new(config).find(included_patterns, excluded_patterns)
70
74
  end
71
75
  end
72
76
  end
@@ -8,19 +8,22 @@ module SlimLint
8
8
  # corresponds to this Sexp.
9
9
  attr_accessor :line
10
10
 
11
- # Creates an {Sexp} from the given {Array}-based Sexp, performing a deep
12
- # copy.
13
- def initialize(sexp)
14
- sexp.each do |child|
15
- item =
16
- case child
17
- when Array
18
- Sexp.new(child)
19
- else
20
- child
21
- end
22
-
23
- push(item)
11
+ # Creates an {Sexp} from the given {Array}-based Sexp.
12
+ #
13
+ # This provides a convenient way to convert between literal arrays of
14
+ # {Symbol}s and {Sexp}s containing {Atom}s and nested {Sexp}s. These objects
15
+ # all expose a similar API that conveniently allows the two objects to be
16
+ # treated similarly due to duck typing.
17
+ #
18
+ # @param array_sexp [Array]
19
+ def initialize(array_sexp)
20
+ array_sexp.each do |atom_or_sexp|
21
+ case atom_or_sexp
22
+ when Array
23
+ push Sexp.new(atom_or_sexp)
24
+ else
25
+ push SlimLint::Atom.new(atom_or_sexp)
26
+ end
24
27
  end
25
28
  end
26
29
 
@@ -42,46 +45,56 @@ module SlimLint
42
45
  # Note that nested Sexps will also be matched, so be careful about the cost
43
46
  # of matching against a complicated pattern.
44
47
  #
45
- # @param sexp_pattern [Sexp]
48
+ # @param sexp_pattern [Object,Array]
46
49
  # @return [Boolean]
47
50
  def match?(sexp_pattern)
51
+ # Delegate matching logic if we're comparing against a matcher
52
+ if sexp_pattern.is_a?(SlimLint::Matcher::Base)
53
+ return sexp_pattern.match?(self)
54
+ end
55
+
48
56
  # If there aren't enough items to compare then this obviously won't match
49
- return unless length >= sexp_pattern.length
57
+ return false unless sexp_pattern.is_a?(Array) && length >= sexp_pattern.length
50
58
 
51
59
  sexp_pattern.each_with_index do |sub_pattern, index|
52
- case sub_pattern
53
- when Array
54
- return false unless self[index].match?(sub_pattern)
55
- else
56
- return false unless self[index] == sub_pattern
57
- end
60
+ return false unless self[index].match?(sub_pattern)
58
61
  end
59
62
 
60
63
  true
61
64
  end
62
65
 
66
+ # Returns pretty-printed representation of this S-expression.
67
+ #
68
+ # @return [String]
69
+ def inspect
70
+ display
71
+ end
72
+
73
+ protected
74
+
75
+ # Pretty-prints this Sexp in a form that is more readable.
76
+ #
77
+ # @param depth [Integer] indentation level to display Sexp at
78
+ # @return [String]
63
79
  def display(depth = 1) # rubocop:disable Metrics/AbcSize
64
80
  indentation = ' ' * 2 * depth
65
- output = indentation
66
81
  output = '['
67
- output << "\n"
68
82
 
69
83
  each_with_index do |nested_sexp, index|
84
+ output << "\n"
70
85
  output += indentation
71
86
 
72
- case nested_sexp
73
- when Sexp
87
+ if nested_sexp.is_a?(SlimLint::Sexp)
74
88
  output += nested_sexp.display(depth + 1)
75
89
  else
76
90
  output += nested_sexp.inspect
77
91
  end
78
92
 
79
- if index < length - 1
80
- output += ",\n"
81
- end
93
+ # Add trailing comma unless this is the last item
94
+ output += ',' if index < length - 1
82
95
  end
83
- output << "\n"
84
- output << ' ' * 2 * (depth - 1)
96
+
97
+ output << "\n" << ' ' * 2 * (depth - 1) unless empty?
85
98
  output << ']'
86
99
 
87
100
  output
@@ -2,42 +2,23 @@ module SlimLint
2
2
  # Provides an interface which when included allows a class to visit nodes in
3
3
  # the Sexp of a Slim document.
4
4
  module SexpVisitor
5
- SexpPattern = Struct.new(:sexp, :callback_method_name)
6
-
7
5
  # Traverse the Sexp looking for matches with registered patterns, firing
8
6
  # callbacks for all matches.
9
7
  #
10
- # @param sexp [Sexp]
8
+ # @param sexp [SlimLint::Sexp]
11
9
  def trigger_pattern_callbacks(sexp)
12
- on_start sexp
10
+ return if on_start(sexp) == :stop
13
11
  traverse sexp
14
12
  end
15
13
 
16
14
  # Traverse the given Sexp, firing callbacks if they are defined.
17
15
  #
18
- # @param sexp [Sexp]
16
+ # @param sexp [SlimLint::Sexp]
19
17
  def traverse(sexp)
20
- block_called = false
21
-
22
- # Define a block within the closure of this method so that pattern matcher
23
- # blocks can call `yield` within their block definitions to force
24
- # traversal of their children.
25
- block = ->(action = :descend) do
26
- block_called = true
27
- case action
28
- when Sexp
29
- # User explicitly yielded a Sexp, indicating they want to control the
30
- # flow of traversal. Traverse the Sexp they returned.
31
- traverse(action)
32
- when :descend
33
- traverse_children(sexp)
34
- end
35
- end
36
-
37
18
  patterns.each do |pattern|
38
19
  next unless sexp.match?(pattern.sexp)
39
20
 
40
- result = method(pattern.callback_method_name).call(sexp, &block)
21
+ result = method(pattern.callback_method_name).call(sexp)
41
22
 
42
23
  # Returning :stop indicates we should stop searching this Sexp
43
24
  # (i.e. stop descending this branch of depth-first search).
@@ -45,42 +26,74 @@ module SlimLint
45
26
  return if result == :stop # rubocop:disable Lint/NonLocalExitFromIterator
46
27
  end
47
28
 
48
- # If no pattern matchers called `yield` explicitly, continue traversing
49
- # children by default (matchers can return `:stop` to not continue).
50
- traverse_children(sexp) unless block_called
29
+ # Continue traversing children by default (match blocks can return `:stop`
30
+ # to not continue).
31
+ traverse_children(sexp)
51
32
  end
52
33
 
34
+ # Traverse the children of this {Sexp}.
35
+ #
36
+ # @param sexp [SlimLint::Sexp]
53
37
  def traverse_children(sexp)
54
38
  sexp.each do |nested_sexp|
55
39
  traverse nested_sexp if nested_sexp.is_a?(Sexp)
56
40
  end
57
41
  end
58
42
 
43
+ # Returns the map of capture names to captured values.
44
+ #
45
+ # @return [Hash, CaptureMap]
46
+ def captures
47
+ self.class.captures || {}
48
+ end
49
+
59
50
  # Returns the list of registered Sexp patterns.
51
+ #
52
+ # @return [Array<SlimLint::SexpVisitor::SexpPattern>]
60
53
  def patterns
61
54
  self.class.patterns || []
62
55
  end
63
56
 
64
57
  # Executed before searching for any pattern matches.
65
58
  #
66
- # @param sexp [SlimLint::Sexp]
59
+ # @param sexp [SlimLint::Sexp] see {SexpVisitor::DSL.on_start}
60
+ # @return [Symbol] see {SexpVisitor::DSL.on_start}
67
61
  def on_start(*)
68
62
  # Overidden by DSL.on_start
69
63
  end
70
64
 
65
+ # Mapping of Sexp pattern to callback method name.
66
+ #
67
+ # @attr_reader sexp [Array] S-expression pattern that when matched triggers the
68
+ # callback
69
+ # @attr_reader callback_method_name [Symbol] name of the method to call when pattern is matched
70
+ SexpPattern = Struct.new(:sexp, :callback_method_name)
71
+ private_constant :SexpPattern
72
+
71
73
  # Exposes a convenient Domain-specific Language (DSL) that makes declaring
72
74
  # Sexp match patterns very easy.
73
75
  #
74
76
  # Include them with `extend SlimLint::SexpVisitor::DSL`
75
77
  module DSL
76
78
  # Registered patterns that this visitor will look for when traversing the
77
- # Sexp.
79
+ # {SlimLint::Sexp}.
78
80
  attr_reader :patterns
79
81
 
82
+ # @return [Hash] map of capture names to captured values
83
+ attr_reader :captures
84
+
80
85
  # DSL helper that defines a sexp pattern and block that will be executed if
81
86
  # the given pattern is found.
82
87
  #
83
88
  # @param sexp_pattern [Sexp]
89
+ # @yield block to execute when the specified pattern is matched
90
+ # @yieldparam sexp [SlimLint::Sexp] Sexp that matched the pattern
91
+ # @yieldreturn [SlimLint::Sexp,Symbol,void]
92
+ # If a Sexp is returned, indicates that traversal should jump directly
93
+ # to that Sexp.
94
+ # If `:stop` is returned, halts further traversal down this branch
95
+ # (i.e. stops recursing, but traversal at higher levels will continue).
96
+ # Otherwise traversal will continue as normal.
84
97
  def on(sexp_pattern, &block)
85
98
  # TODO: Index Sexps on creation so we can quickly jump to potential
86
99
  # matches instead of checking array.
@@ -97,9 +110,34 @@ module SlimLint
97
110
  end
98
111
 
99
112
  # Define a block of code to run before checking for any pattern matches.
113
+ #
114
+ # @yield block to execute
115
+ # @yieldparam sexp [SlimLint::Sexp] the root Sexp
116
+ # @yieldreturn [Symbol] if `:stop`, indicates that no further processing
117
+ # should occur
100
118
  def on_start(&block)
101
119
  define_method(:on_start, block)
102
120
  end
121
+
122
+ # Represents a pattern that matches anything.
123
+ #
124
+ # @return [SlimLint::Matcher::Anything]
125
+ def anything
126
+ SlimLint::Matcher::Anything.new
127
+ end
128
+
129
+ # Represents a pattern that matches the specified matcher, storing the
130
+ # matched value in the captures list under the given name.
131
+ #
132
+ # @param capture_name [Symbol]
133
+ # @param matcher [SlimLint::Matcher::Base]
134
+ # @return [SlimLint::Matcher::Capture]
135
+ def capture(capture_name, matcher)
136
+ @captures ||= SlimLint::CaptureMap.new
137
+
138
+ @captures[capture_name] =
139
+ SlimLint::Matcher::Capture.from_matcher(matcher)
140
+ end
103
141
  end
104
142
  end
105
143
  end
@@ -3,6 +3,23 @@ module SlimLint
3
3
  module Utils
4
4
  module_function
5
5
 
6
+ # Returns whether a glob pattern (or any of a list of patterns) matches the
7
+ # specified file.
8
+ #
9
+ # This is defined here so our file globbing options are consistent
10
+ # everywhere we perform globbing.
11
+ #
12
+ # @param glob [String, Array]
13
+ # @param file [String]
14
+ # @return [Boolean]
15
+ def any_glob_matches?(globs_or_glob, file)
16
+ Array(globs_or_glob).any? do |glob|
17
+ ::File.fnmatch?(glob, file,
18
+ ::File::FNM_PATHNAME | # Wildcards don't match path separators
19
+ ::File::FNM_DOTMATCH) # `*` wildcard matches dotfiles
20
+ end
21
+ end
22
+
6
23
  # Find all consecutive items satisfying the given block of a minimum size,
7
24
  # yielding each group of consecutive items to the provided block.
8
25
  #
@@ -1,4 +1,4 @@
1
1
  # Defines the gem version.
2
2
  module SlimLint
3
- VERSION = '0.2.0'
3
+ VERSION = '0.3.0'
4
4
  end
data/lib/slim_lint.rb CHANGED
@@ -6,13 +6,14 @@ require 'slim_lint/exceptions'
6
6
  require 'slim_lint/configuration'
7
7
  require 'slim_lint/configuration_loader'
8
8
  require 'slim_lint/utils'
9
+ require 'slim_lint/atom'
9
10
  require 'slim_lint/sexp'
10
11
  require 'slim_lint/file_finder'
11
12
  require 'slim_lint/linter_registry'
12
13
  require 'slim_lint/logger'
13
14
  require 'slim_lint/version'
14
15
 
15
- # Need to load slim before we can
16
+ # Need to load slim before we can define filters
16
17
  require 'slim'
17
18
 
18
19
  # Load all filters (required by SlimLint::Engine)
@@ -22,14 +23,22 @@ end
22
23
 
23
24
  require 'slim_lint/engine'
24
25
  require 'slim_lint/document'
26
+ require 'slim_lint/capture_map'
25
27
  require 'slim_lint/sexp_visitor'
26
28
  require 'slim_lint/lint'
27
29
  require 'slim_lint/ruby_parser'
28
30
  require 'slim_lint/linter'
29
31
  require 'slim_lint/reporter'
30
32
  require 'slim_lint/report'
33
+ require 'slim_lint/linter_selector'
31
34
  require 'slim_lint/runner'
32
35
 
36
+ # Load all matchers
37
+ require 'slim_lint/matcher/base'
38
+ Dir[File.expand_path('slim_lint/matcher/*.rb', File.dirname(__FILE__))].each do |file|
39
+ require file
40
+ end
41
+
33
42
  # Load all linters
34
43
  Dir[File.expand_path('slim_lint/linter/*.rb', File.dirname(__FILE__))].each do |file|
35
44
  require file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slim_lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane da Silva
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-20 00:00:00.000000000 Z
11
+ date: 2015-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: slim
@@ -91,6 +91,8 @@ files:
91
91
  - bin/slim-lint
92
92
  - config/default.yml
93
93
  - lib/slim_lint.rb
94
+ - lib/slim_lint/atom.rb
95
+ - lib/slim_lint/capture_map.rb
94
96
  - lib/slim_lint/cli.rb
95
97
  - lib/slim_lint/configuration.rb
96
98
  - lib/slim_lint/configuration_loader.rb
@@ -112,7 +114,12 @@ files:
112
114
  - lib/slim_lint/linter/tag_case.rb
113
115
  - lib/slim_lint/linter/trailing_whitespace.rb
114
116
  - lib/slim_lint/linter_registry.rb
117
+ - lib/slim_lint/linter_selector.rb
115
118
  - lib/slim_lint/logger.rb
119
+ - lib/slim_lint/matcher/anything.rb
120
+ - lib/slim_lint/matcher/base.rb
121
+ - lib/slim_lint/matcher/capture.rb
122
+ - lib/slim_lint/matcher/nothing.rb
116
123
  - lib/slim_lint/options.rb
117
124
  - lib/slim_lint/rake_task.rb
118
125
  - lib/slim_lint/report.rb
@@ -147,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
154
  version: '0'
148
155
  requirements: []
149
156
  rubyforge_project:
150
- rubygems_version: 2.2.2
157
+ rubygems_version: 2.4.5
151
158
  signing_key:
152
159
  specification_version: 4
153
160
  summary: Slim template linting tool