slim_lint 0.1.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.
@@ -0,0 +1,91 @@
1
+ module SlimLint
2
+ # Utility class for extracting Ruby script from a Slim template that can then
3
+ # be linted with a Ruby linter (i.e. is "legal" Ruby).
4
+ #
5
+ # The goal is to turn this:
6
+ #
7
+ # - if items.any?
8
+ # table#items
9
+ # - for item in items
10
+ # tr
11
+ # td.name = item.name
12
+ # td.price = item.price
13
+ # - else
14
+ # p No items found.
15
+ #
16
+ # into (something like) this:
17
+ #
18
+ # if items.any?
19
+ # for item in items
20
+ # puts item.name
21
+ # puts item.price
22
+ # else
23
+ # puts 'No items found'
24
+ # end
25
+ #
26
+ # The translation won't be perfect, and won't make any real sense, but the
27
+ # relationship between variable declarations/uses and the flow control graph
28
+ # will remain intact.
29
+ class RubyExtractor
30
+ include SexpVisitor
31
+ extend SexpVisitor::DSL
32
+
33
+ attr_reader :source_map
34
+
35
+ # Extracts Ruby code from Sexp representing a Slim document.
36
+ #
37
+ # @param sexp [SlimLint::Sexp]
38
+ def extract(sexp)
39
+ trigger_pattern_callbacks(sexp)
40
+ @source_lines.join("\n")
41
+ end
42
+
43
+ on_start do |_sexp|
44
+ @source_lines = []
45
+ @source_map = {}
46
+ @line_count = 0
47
+ end
48
+
49
+ on [:html, :doctype] do |sexp|
50
+ append('puts', sexp)
51
+ end
52
+
53
+ on [:html, :tag] do |sexp|
54
+ append('puts', sexp)
55
+ end
56
+
57
+ on [:static] do |sexp|
58
+ append('puts', sexp)
59
+ end
60
+
61
+ on [:dynamic] do |sexp|
62
+ _, ruby = sexp
63
+ append(ruby, sexp)
64
+ end
65
+
66
+ on [:code] do |sexp|
67
+ _, ruby = sexp
68
+ append(ruby, sexp)
69
+ end
70
+
71
+ private
72
+
73
+ # Append code to the buffer.
74
+ #
75
+ # @param code [String]
76
+ # @param sexp [SlimLint::Sexp]
77
+ def append(code, sexp)
78
+ return if code.empty?
79
+
80
+ @source_lines << code
81
+ original_line = sexp.line
82
+
83
+ # For code that spans multiple lines, the resulting code will span
84
+ # multiple lines, so we need to create a mapping for each line.
85
+ (code.count("\n") + 1).times do
86
+ @line_count += 1
87
+ @source_map[@line_count] = original_line
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,29 @@
1
+ require 'astrolabe/builder'
2
+ require 'parser/current'
3
+
4
+ module SlimLint
5
+ # Parser for the Ruby language.
6
+ #
7
+ # This provides a convenient wrapper around the `parser` gem and the
8
+ # `astrolabe` integration to go with it. It is intended to be used for linter
9
+ # checks that require deep inspection of Ruby code.
10
+ class RubyParser
11
+ # Creates a reusable parser.
12
+ def initialize
13
+ @builder = ::Astrolabe::Builder.new
14
+ @parser = ::Parser::CurrentRuby.new(@builder)
15
+ end
16
+
17
+ # Parse the given Ruby source into an abstract syntax tree.
18
+ #
19
+ # @param source [String] Ruby source code
20
+ # @return [Array] syntax tree in the form returned by Parser gem
21
+ def parse(source)
22
+ buffer = ::Parser::Source::Buffer.new('(string)')
23
+ buffer.source = source
24
+
25
+ @parser.reset
26
+ @parser.parse(buffer)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,72 @@
1
+ module SlimLint
2
+ # Responsible for running the applicable linters against the desired files.
3
+ class Runner
4
+ # Make the list of applicable files available
5
+ attr_reader :files
6
+
7
+ # Runs the appropriate linters against the desired files given the specified
8
+ # options.
9
+ #
10
+ # @param options [Hash]
11
+ # @raise [SlimLint::Exceptions::NoLintersError] when no linters are enabled
12
+ # @return [SlimLint::Report] a summary of all lints found
13
+ def run(options = {})
14
+ config = load_applicable_config(options)
15
+ files = extract_applicable_files(options, config)
16
+ linters = extract_enabled_linters(config, options)
17
+
18
+ raise SlimLint::Exceptions::NoLintersError, 'No linters specified' if linters.empty?
19
+
20
+ @lints = []
21
+ files.each do |file|
22
+ find_lints(file, linters, config)
23
+ end
24
+
25
+ SlimLint::Report.new(@lints, files)
26
+ end
27
+
28
+ private
29
+
30
+ def load_applicable_config(options)
31
+ if options[:config_file]
32
+ SlimLint::ConfigurationLoader.load_file(options[:config_file])
33
+ else
34
+ SlimLint::ConfigurationLoader.load_applicable_config
35
+ end
36
+ end
37
+
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)
60
+ end
61
+ rescue Slim::Parser::SyntaxError => ex
62
+ @lints << Lint.new(nil, file, ex.line, ex.error, :error)
63
+ end
64
+
65
+ def extract_applicable_files(options, config)
66
+ included_patterns = options[:files]
67
+ excluded_files = options.fetch(:excluded_files, [])
68
+
69
+ SlimLint::FileFinder.new(config).find(included_patterns, excluded_files)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,90 @@
1
+ module SlimLint
2
+ # Symbolic expression which represents tree-structured data.
3
+ #
4
+ # The main use of this particular implementation is to provide a single
5
+ # location for defining convenience helpers when operating on Sexps.
6
+ class Sexp < Array
7
+ # Stores the line number of the code in the original document that
8
+ # corresponds to this Sexp.
9
+ attr_accessor :line
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)
24
+ end
25
+ end
26
+
27
+ # Returns whether this {Sexp} matches the given Sexp pattern.
28
+ #
29
+ # A Sexp pattern is simply an incomplete Sexp prefix.
30
+ #
31
+ # @example
32
+ # The following Sexp:
33
+ #
34
+ # [:html, :doctype, "html5"]
35
+ #
36
+ # ...will match the given patterns:
37
+ #
38
+ # [:html]
39
+ # [:html, :doctype]
40
+ # [:html, :doctype, "html5"]
41
+ #
42
+ # Note that nested Sexps will also be matched, so be careful about the cost
43
+ # of matching against a complicated pattern.
44
+ #
45
+ # @param sexp_pattern [Sexp]
46
+ # @return [Boolean]
47
+ def match?(sexp_pattern)
48
+ # If there aren't enough items to compare then this obviously won't match
49
+ return unless length >= sexp_pattern.length
50
+
51
+ 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
58
+ end
59
+
60
+ true
61
+ end
62
+
63
+ def display(depth = 1) # rubocop:disable Metrics/AbcSize
64
+ indentation = ' ' * 2 * depth
65
+ output = indentation
66
+ output = '['
67
+ output << "\n"
68
+
69
+ each_with_index do |nested_sexp, index|
70
+ output += indentation
71
+
72
+ case nested_sexp
73
+ when Sexp
74
+ output += nested_sexp.display(depth + 1)
75
+ else
76
+ output += nested_sexp.inspect
77
+ end
78
+
79
+ if index < length - 1
80
+ output += ",\n"
81
+ end
82
+ end
83
+ output << "\n"
84
+ output << ' ' * 2 * (depth - 1)
85
+ output << ']'
86
+
87
+ output
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,105 @@
1
+ module SlimLint
2
+ # Provides an interface which when included allows a class to visit nodes in
3
+ # the Sexp of a Slim document.
4
+ module SexpVisitor
5
+ SexpPattern = Struct.new(:sexp, :callback_method_name)
6
+
7
+ # Traverse the Sexp looking for matches with registered patterns, firing
8
+ # callbacks for all matches.
9
+ #
10
+ # @param sexp [Sexp]
11
+ def trigger_pattern_callbacks(sexp)
12
+ on_start sexp
13
+ traverse sexp
14
+ end
15
+
16
+ # Traverse the given Sexp, firing callbacks if they are defined.
17
+ #
18
+ # @param sexp [Sexp]
19
+ 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
+ patterns.each do |pattern|
38
+ next unless sexp.match?(pattern.sexp)
39
+
40
+ result = method(pattern.callback_method_name).call(sexp, &block)
41
+
42
+ # Returning :stop indicates we should stop searching this Sexp
43
+ # (i.e. stop descending this branch of depth-first search).
44
+ # The `return` here is very intentional.
45
+ return if result == :stop # rubocop:disable Lint/NonLocalExitFromIterator
46
+ end
47
+
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
51
+ end
52
+
53
+ def traverse_children(sexp)
54
+ sexp.each do |nested_sexp|
55
+ traverse nested_sexp if nested_sexp.is_a?(Sexp)
56
+ end
57
+ end
58
+
59
+ # Returns the list of registered Sexp patterns.
60
+ def patterns
61
+ self.class.patterns || []
62
+ end
63
+
64
+ # Executed before searching for any pattern matches.
65
+ #
66
+ # @param sexp [SlimLint::Sexp]
67
+ def on_start(*)
68
+ # Overidden by DSL.on_start
69
+ end
70
+
71
+ # Exposes a convenient Domain-specific Language (DSL) that makes declaring
72
+ # Sexp match patterns very easy.
73
+ #
74
+ # Include them with `extend SlimLint::SexpVisitor::DSL`
75
+ module DSL
76
+ # Registered patterns that this visitor will look for when traversing the
77
+ # Sexp.
78
+ attr_reader :patterns
79
+
80
+ # DSL helper that defines a sexp pattern and block that will be executed if
81
+ # the given pattern is found.
82
+ #
83
+ # @param sexp_pattern [Sexp]
84
+ def on(sexp_pattern, &block)
85
+ # TODO: Index Sexps on creation so we can quickly jump to potential
86
+ # matches instead of checking array.
87
+ @patterns ||= []
88
+ @pattern_number ||= 1
89
+
90
+ # Use a monotonically increasing number to identify the method so that in
91
+ # debugging we can simply look at the nth defintion in the class.
92
+ unique_method_name = :"on_pattern_#{@pattern_number}"
93
+ define_method(unique_method_name, block)
94
+
95
+ @pattern_number += 1
96
+ @patterns << SexpPattern.new(sexp_pattern, unique_method_name)
97
+ end
98
+
99
+ # Define a block of code to run before checking for any pattern matches.
100
+ def on_start(&block)
101
+ define_method(:on_start, block)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,4 @@
1
+ # Defines the gem version.
2
+ module SlimLint
3
+ VERSION = '0.1.0'
4
+ end
data/lib/slim_lint.rb ADDED
@@ -0,0 +1,40 @@
1
+ # Load all slim-lint modules necessary to parse and lint a file.
2
+ # Ordering here can be important depending on class references in each module.
3
+
4
+ require 'slim_lint/constants'
5
+ require 'slim_lint/exceptions'
6
+ require 'slim_lint/configuration'
7
+ require 'slim_lint/configuration_loader'
8
+ require 'slim_lint/sexp'
9
+ require 'slim_lint/file_finder'
10
+ require 'slim_lint/linter_registry'
11
+ require 'slim_lint/logger'
12
+ require 'slim_lint/version'
13
+
14
+ # Need to load slim before we can
15
+ require 'slim'
16
+
17
+ # Load all filters (required by SlimLint::Engine)
18
+ Dir[File.expand_path('slim_lint/filters/*.rb', File.dirname(__FILE__))].each do |file|
19
+ require file
20
+ end
21
+
22
+ require 'slim_lint/engine'
23
+ require 'slim_lint/document'
24
+ require 'slim_lint/sexp_visitor'
25
+ require 'slim_lint/lint'
26
+ require 'slim_lint/ruby_parser'
27
+ require 'slim_lint/linter'
28
+ require 'slim_lint/reporter'
29
+ require 'slim_lint/report'
30
+ require 'slim_lint/runner'
31
+
32
+ # Load all linters
33
+ Dir[File.expand_path('slim_lint/linter/*.rb', File.dirname(__FILE__))].each do |file|
34
+ require file
35
+ end
36
+
37
+ # Load all reporters
38
+ Dir[File.expand_path('slim_lint/reporter/*.rb', File.dirname(__FILE__))].each do |file|
39
+ require file
40
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slim_lint
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shane da Silva
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: slim
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.25.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.25.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: sysexits
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-its
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
83
+ description: Configurable tool for writing clean and consistent Slim templates
84
+ email:
85
+ - shane@dasilva.io
86
+ executables:
87
+ - slim-lint
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - bin/slim-lint
92
+ - config/default.yml
93
+ - lib/slim_lint.rb
94
+ - lib/slim_lint/cli.rb
95
+ - lib/slim_lint/configuration.rb
96
+ - lib/slim_lint/configuration_loader.rb
97
+ - lib/slim_lint/constants.rb
98
+ - lib/slim_lint/document.rb
99
+ - lib/slim_lint/engine.rb
100
+ - lib/slim_lint/exceptions.rb
101
+ - lib/slim_lint/file_finder.rb
102
+ - lib/slim_lint/filters/inject_line_numbers.rb
103
+ - lib/slim_lint/filters/sexp_converter.rb
104
+ - lib/slim_lint/lint.rb
105
+ - lib/slim_lint/linter.rb
106
+ - lib/slim_lint/linter/line_length.rb
107
+ - lib/slim_lint/linter/redundant_div.rb
108
+ - lib/slim_lint/linter/rubocop.rb
109
+ - lib/slim_lint/linter/trailing_whitespace.rb
110
+ - lib/slim_lint/linter_registry.rb
111
+ - lib/slim_lint/logger.rb
112
+ - lib/slim_lint/options.rb
113
+ - lib/slim_lint/rake_task.rb
114
+ - lib/slim_lint/report.rb
115
+ - lib/slim_lint/reporter.rb
116
+ - lib/slim_lint/reporter/default_reporter.rb
117
+ - lib/slim_lint/reporter/json_reporter.rb
118
+ - lib/slim_lint/ruby_extract_engine.rb
119
+ - lib/slim_lint/ruby_extractor.rb
120
+ - lib/slim_lint/ruby_parser.rb
121
+ - lib/slim_lint/runner.rb
122
+ - lib/slim_lint/sexp.rb
123
+ - lib/slim_lint/sexp_visitor.rb
124
+ - lib/slim_lint/version.rb
125
+ homepage: https://github.com/sds/slim-lint
126
+ licenses:
127
+ - MIT
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: 2.0.0
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubyforge_project:
145
+ rubygems_version: 2.2.2
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: Slim template linting tool
149
+ test_files: []