slim_lint_standard 0.0.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/LICENSE.md +21 -0
- data/bin/slim-lint-standard +7 -0
- data/config/default.yml +109 -0
- data/lib/slim_lint/atom.rb +129 -0
- data/lib/slim_lint/capture_map.rb +19 -0
- data/lib/slim_lint/cli.rb +167 -0
- data/lib/slim_lint/configuration.rb +111 -0
- data/lib/slim_lint/configuration_loader.rb +86 -0
- data/lib/slim_lint/constants.rb +10 -0
- data/lib/slim_lint/document.rb +78 -0
- data/lib/slim_lint/engine.rb +41 -0
- data/lib/slim_lint/exceptions.rb +20 -0
- data/lib/slim_lint/file_finder.rb +88 -0
- data/lib/slim_lint/filter.rb +126 -0
- data/lib/slim_lint/filters/attribute_processor.rb +46 -0
- data/lib/slim_lint/filters/auto_indenter.rb +39 -0
- data/lib/slim_lint/filters/control_processor.rb +46 -0
- data/lib/slim_lint/filters/do_inserter.rb +39 -0
- data/lib/slim_lint/filters/end_inserter.rb +74 -0
- data/lib/slim_lint/filters/interpolation.rb +73 -0
- data/lib/slim_lint/filters/multi_flattener.rb +32 -0
- data/lib/slim_lint/filters/splat_processor.rb +20 -0
- data/lib/slim_lint/filters/static_merger.rb +47 -0
- data/lib/slim_lint/lint.rb +70 -0
- data/lib/slim_lint/linter/avoid_multiline_expressions.rb +41 -0
- data/lib/slim_lint/linter/comment_control_statement.rb +26 -0
- data/lib/slim_lint/linter/consecutive_control_statements.rb +26 -0
- data/lib/slim_lint/linter/control_statement_spacing.rb +32 -0
- data/lib/slim_lint/linter/dynamic_output_spacing.rb +77 -0
- data/lib/slim_lint/linter/embedded_engines.rb +18 -0
- data/lib/slim_lint/linter/empty_control_statement.rb +15 -0
- data/lib/slim_lint/linter/empty_lines.rb +24 -0
- data/lib/slim_lint/linter/file_length.rb +18 -0
- data/lib/slim_lint/linter/line_length.rb +18 -0
- data/lib/slim_lint/linter/redundant_div.rb +21 -0
- data/lib/slim_lint/linter/rubocop.rb +131 -0
- data/lib/slim_lint/linter/standard.rb +69 -0
- data/lib/slim_lint/linter/tab.rb +20 -0
- data/lib/slim_lint/linter/tag_case.rb +15 -0
- data/lib/slim_lint/linter/trailing_blank_lines.rb +19 -0
- data/lib/slim_lint/linter/trailing_whitespace.rb +17 -0
- data/lib/slim_lint/linter.rb +93 -0
- data/lib/slim_lint/linter_registry.rb +37 -0
- data/lib/slim_lint/linter_selector.rb +87 -0
- data/lib/slim_lint/logger.rb +103 -0
- data/lib/slim_lint/matcher/anything.rb +11 -0
- data/lib/slim_lint/matcher/base.rb +21 -0
- data/lib/slim_lint/matcher/capture.rb +32 -0
- data/lib/slim_lint/matcher/nothing.rb +13 -0
- data/lib/slim_lint/options.rb +110 -0
- data/lib/slim_lint/parser.rb +584 -0
- data/lib/slim_lint/rake_task.rb +125 -0
- data/lib/slim_lint/report.rb +25 -0
- data/lib/slim_lint/reporter/checkstyle_reporter.rb +42 -0
- data/lib/slim_lint/reporter/default_reporter.rb +40 -0
- data/lib/slim_lint/reporter/emacs_reporter.rb +40 -0
- data/lib/slim_lint/reporter/json_reporter.rb +50 -0
- data/lib/slim_lint/reporter.rb +44 -0
- data/lib/slim_lint/ruby_extract_engine.rb +30 -0
- data/lib/slim_lint/ruby_extractor.rb +175 -0
- data/lib/slim_lint/ruby_parser.rb +32 -0
- data/lib/slim_lint/runner.rb +82 -0
- data/lib/slim_lint/sexp.rb +134 -0
- data/lib/slim_lint/sexp_visitor.rb +150 -0
- data/lib/slim_lint/source_location.rb +45 -0
- data/lib/slim_lint/utils.rb +84 -0
- data/lib/slim_lint/version.rb +6 -0
- data/lib/slim_lint.rb +55 -0
- metadata +218 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Contains information about all lints detected during a scan.
|
5
|
+
class Report
|
6
|
+
# List of lints that were found.
|
7
|
+
attr_accessor :lints
|
8
|
+
|
9
|
+
# List of files that were linted.
|
10
|
+
attr_reader :files
|
11
|
+
|
12
|
+
# Creates a report.
|
13
|
+
#
|
14
|
+
# @param lints [Array<SlimLint::Lint>] lints that were found
|
15
|
+
# @param files [Array<String>] files that were linted
|
16
|
+
def initialize(lints, files)
|
17
|
+
@lints = lints.sort_by { |l| [l.filename, l.line] }
|
18
|
+
@files = files
|
19
|
+
end
|
20
|
+
|
21
|
+
def failed?
|
22
|
+
@lints.any?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rexml/document"
|
4
|
+
|
5
|
+
module SlimLint
|
6
|
+
# Outputs report as a Checkstyle XML document.
|
7
|
+
class Reporter::CheckstyleReporter < Reporter
|
8
|
+
def display_report(report)
|
9
|
+
document = REXML::Document.new.tap do |d|
|
10
|
+
d << REXML::XMLDecl.new
|
11
|
+
end
|
12
|
+
checkstyle = REXML::Element.new("checkstyle", document)
|
13
|
+
|
14
|
+
report.lints.group_by(&:filename).map do |lint|
|
15
|
+
map_file(lint, checkstyle)
|
16
|
+
end
|
17
|
+
|
18
|
+
log.log document.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def map_file(file, checkstyle)
|
24
|
+
REXML::Element.new("file", checkstyle).tap do |f|
|
25
|
+
path_name = file.first
|
26
|
+
path_name = relative_path(file) if defined?(relative_path)
|
27
|
+
f.attributes["name"] = path_name
|
28
|
+
|
29
|
+
file.last.map { |o| map_offense(o, f) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def map_offense(offence, parent)
|
34
|
+
REXML::Element.new("error", parent).tap do |e|
|
35
|
+
e.attributes["line"] = offence.line
|
36
|
+
e.attributes["severity"] = offence.error? ? "error" : "warning"
|
37
|
+
e.attributes["message"] = offence.message
|
38
|
+
e.attributes["source"] = "slim-lint"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Outputs lints in a simple format with the filename, line number, and lint
|
5
|
+
# message.
|
6
|
+
class Reporter::DefaultReporter < Reporter
|
7
|
+
def display_report(report)
|
8
|
+
sorted_lints = report.lints.sort_by { |l| [l.filename, l.line] }
|
9
|
+
|
10
|
+
sorted_lints.each do |lint|
|
11
|
+
print_location(lint)
|
12
|
+
print_type(lint)
|
13
|
+
print_message(lint)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def print_location(lint)
|
20
|
+
log.info lint.filename, false
|
21
|
+
log.log ":", false
|
22
|
+
log.bold lint.line, false
|
23
|
+
log.log ":", false
|
24
|
+
log.bold lint.column, false
|
25
|
+
end
|
26
|
+
|
27
|
+
def print_type(lint)
|
28
|
+
if lint.error?
|
29
|
+
log.error " [E] ", false
|
30
|
+
else
|
31
|
+
log.warning " [W] ", false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def print_message(lint)
|
36
|
+
log.success("#{lint.name}: ", false)
|
37
|
+
log.log lint.message
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Outputs lints in format: {filename}:{line}:{column}: {kind}: {message}.
|
5
|
+
class Reporter::EmacsReporter < Reporter
|
6
|
+
def display_report(report)
|
7
|
+
sorted_lints = report.lints.sort_by { |l| [l.filename, l.line] }
|
8
|
+
|
9
|
+
sorted_lints.each do |lint|
|
10
|
+
print_location(lint)
|
11
|
+
print_type(lint)
|
12
|
+
print_message(lint)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def print_location(lint)
|
19
|
+
log.info lint.filename, false
|
20
|
+
log.log ":", false
|
21
|
+
log.bold lint.line, false
|
22
|
+
log.log ":", false
|
23
|
+
log.bold lint.column, false
|
24
|
+
log.log ":", false
|
25
|
+
end
|
26
|
+
|
27
|
+
def print_type(lint)
|
28
|
+
if lint.error?
|
29
|
+
log.error " E: ", false
|
30
|
+
else
|
31
|
+
log.warning " W: ", false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def print_message(lint)
|
36
|
+
log.success("#{lint.name}: ", false)
|
37
|
+
log.log lint.message
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Outputs report as a JSON document.
|
5
|
+
class Reporter::JsonReporter < Reporter
|
6
|
+
def display_report(report)
|
7
|
+
lints = report.lints
|
8
|
+
grouped = lints.group_by(&:filename)
|
9
|
+
|
10
|
+
report_hash = {
|
11
|
+
metadata: metadata,
|
12
|
+
files: grouped.map { |l| map_file(l) },
|
13
|
+
summary: {
|
14
|
+
offense_count: lints.length,
|
15
|
+
target_file_count: grouped.length,
|
16
|
+
inspected_file_count: report.files.length
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
log.log report_hash.to_json
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def metadata
|
26
|
+
{
|
27
|
+
slim_lint_version: SlimLint::VERSION,
|
28
|
+
ruby_engine: RUBY_ENGINE,
|
29
|
+
ruby_patchlevel: RUBY_PATCHLEVEL.to_s,
|
30
|
+
ruby_platform: RUBY_PLATFORM
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def map_file(file)
|
35
|
+
{
|
36
|
+
path: file.first,
|
37
|
+
offenses: file.last.map { |o| map_offense(o) }
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def map_offense(offense)
|
42
|
+
{
|
43
|
+
severity: offense.severity,
|
44
|
+
message: offense.message,
|
45
|
+
location: offense.location.as_json,
|
46
|
+
cop_name: offense.name
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Abstract lint reporter. Subclass and override {#display_report} to
|
5
|
+
# implement a custom lint reporter.
|
6
|
+
#
|
7
|
+
# @abstract
|
8
|
+
class Reporter
|
9
|
+
# Creates the reporter that will display the given report.
|
10
|
+
#
|
11
|
+
# @param logger [SlimLint::Logger]
|
12
|
+
def initialize(logger)
|
13
|
+
@log = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
# Implemented by subclasses to display lints from a {SlimLint::Report}.
|
17
|
+
#
|
18
|
+
# @param report [SlimLint::Report]
|
19
|
+
def display_report(report)
|
20
|
+
raise NotImplementedError,
|
21
|
+
"Implement `display_report` to display #{report}"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Keep tracking all the descendants of this class for the list of available
|
25
|
+
# reporters.
|
26
|
+
#
|
27
|
+
# @return [Array<Class>]
|
28
|
+
def self.descendants
|
29
|
+
@descendants ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
# Executed when this class is subclassed.
|
33
|
+
#
|
34
|
+
# @param descendant [Class]
|
35
|
+
def self.inherited(descendant)
|
36
|
+
descendants << descendant
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# @return [SlimLint::Logger] logger to send output to
|
42
|
+
attr_reader :log
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Generates a {SlimLint::Sexp} suitable for consumption by the
|
5
|
+
# {RubyExtractor}.
|
6
|
+
#
|
7
|
+
# This is mostly copied from Slim::Engine, with some filters and generators
|
8
|
+
# omitted.
|
9
|
+
class RubyExtractEngine < Temple::Engine
|
10
|
+
filter :Encoding
|
11
|
+
filter :RemoveBOM
|
12
|
+
|
13
|
+
# Parse into S-expression using Slim parser
|
14
|
+
use SlimLint::Parser
|
15
|
+
|
16
|
+
# Perform additional processing so extracting Ruby code in {RubyExtractor}
|
17
|
+
# is easier. We don't do this for regular linters because some information
|
18
|
+
# about the original syntax tree is lost in the process, but that doesn't
|
19
|
+
# matter in this case.
|
20
|
+
use SlimLint::Filters::Interpolation
|
21
|
+
use SlimLint::Filters::SplatProcessor
|
22
|
+
use SlimLint::Filters::DoInserter
|
23
|
+
use SlimLint::Filters::EndInserter
|
24
|
+
use SlimLint::Filters::AutoIndenter
|
25
|
+
use SlimLint::Filters::ControlProcessor
|
26
|
+
use SlimLint::Filters::AttributeProcessor
|
27
|
+
use SlimLint::Filters::MultiFlattener
|
28
|
+
use SlimLint::Filters::StaticMerger
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Utility class for extracting Ruby script from a Slim template that can then
|
5
|
+
# be linted with a Ruby linter (i.e. is "legal" Ruby).
|
6
|
+
#
|
7
|
+
# The goal is to turn this:
|
8
|
+
#
|
9
|
+
# - if items.any?
|
10
|
+
# table#items
|
11
|
+
# - for item in items
|
12
|
+
# tr
|
13
|
+
# td.name = item.name
|
14
|
+
# td.price = item.price
|
15
|
+
# - else
|
16
|
+
# p No items found.
|
17
|
+
#
|
18
|
+
# into (something like) this:
|
19
|
+
#
|
20
|
+
# if items.any?
|
21
|
+
# for item in items
|
22
|
+
# puts item.name
|
23
|
+
# puts item.price
|
24
|
+
# else
|
25
|
+
# puts 'No items found'
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# The translation won't be perfect, and won't make any real sense, but the
|
29
|
+
# relationship between variable declarations/uses and the flow control graph
|
30
|
+
# will remain intact.
|
31
|
+
class RubyExtractor
|
32
|
+
include SexpVisitor
|
33
|
+
extend SexpVisitor::DSL
|
34
|
+
|
35
|
+
# Stores the extracted source and a map of lines of generated source to the
|
36
|
+
# original source that created them.
|
37
|
+
#
|
38
|
+
# @attr_reader source [String] generated source code
|
39
|
+
# @attr_reader source_map [Hash] map of line numbers from generated source
|
40
|
+
# to original source line number
|
41
|
+
RubySource = Struct.new(:source, :source_map)
|
42
|
+
|
43
|
+
# Extracts Ruby code from Sexp representing a Slim document.
|
44
|
+
#
|
45
|
+
# @param sexp [SlimLint::Sexp]
|
46
|
+
# @return [SlimLint::RubyExtractor::RubySource]
|
47
|
+
def extract(sexp)
|
48
|
+
trigger_pattern_callbacks(sexp)
|
49
|
+
RubySource.new(@source_lines.join("\n") + "\n", @source_map)
|
50
|
+
end
|
51
|
+
|
52
|
+
on_start do |_sexp|
|
53
|
+
@source_lines = []
|
54
|
+
@source_map = {}
|
55
|
+
@line_count = 0
|
56
|
+
@indent = 0
|
57
|
+
@dummy_puts_count = 0
|
58
|
+
end
|
59
|
+
|
60
|
+
on [:html, :doctype] do |sexp|
|
61
|
+
append_dummy_puts(sexp)
|
62
|
+
end
|
63
|
+
|
64
|
+
on [:html, :tag] do |sexp|
|
65
|
+
append_dummy_puts(sexp)
|
66
|
+
end
|
67
|
+
|
68
|
+
on [:html, :attr] do |sexp|
|
69
|
+
_, _, attr, value = sexp
|
70
|
+
append("attribute(#{attr.value.inspect}) do", attr)
|
71
|
+
@indent += 1
|
72
|
+
traverse(value)
|
73
|
+
@indent -= 1
|
74
|
+
append("end", attr)
|
75
|
+
:stop
|
76
|
+
end
|
77
|
+
|
78
|
+
on [:html, :comment] do |sexp|
|
79
|
+
append_dummy_puts(sexp)
|
80
|
+
:stop
|
81
|
+
end
|
82
|
+
|
83
|
+
on [:html, :condcomment] do |sexp|
|
84
|
+
append_dummy_puts(sexp)
|
85
|
+
:stop
|
86
|
+
end
|
87
|
+
|
88
|
+
on [:slim_lint, :indent] do |sexp|
|
89
|
+
@indent += 1
|
90
|
+
end
|
91
|
+
|
92
|
+
on [:slim_lint, :outdent] do |sexp|
|
93
|
+
@indent -= 1
|
94
|
+
end
|
95
|
+
|
96
|
+
on [:static] do |sexp|
|
97
|
+
append_dummy_puts(sexp)
|
98
|
+
end
|
99
|
+
|
100
|
+
on [:dynamic] do |sexp|
|
101
|
+
_, ruby = sexp
|
102
|
+
append("output do", sexp)
|
103
|
+
@indent += 1
|
104
|
+
traverse_children(ruby)
|
105
|
+
@indent -= 1
|
106
|
+
append("end", sexp)
|
107
|
+
:stop
|
108
|
+
end
|
109
|
+
|
110
|
+
on [:interpolated] do |sexp|
|
111
|
+
_, ruby = sexp
|
112
|
+
append_interpolated(ruby, sexp)
|
113
|
+
end
|
114
|
+
|
115
|
+
on [:code] do |sexp|
|
116
|
+
_, ruby = sexp
|
117
|
+
append(ruby.value, sexp)
|
118
|
+
end
|
119
|
+
|
120
|
+
on [:slim, :embedded] do |sexp|
|
121
|
+
_, _, name, body, _attrs = sexp
|
122
|
+
|
123
|
+
if name == "ruby"
|
124
|
+
body.drop(1).each do |subexp|
|
125
|
+
if subexp[0] == :static
|
126
|
+
append(subexp[1].value, subexp)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
else
|
130
|
+
append_dummy_puts(sexp)
|
131
|
+
end
|
132
|
+
|
133
|
+
:stop
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
# Append code to the buffer.
|
139
|
+
#
|
140
|
+
# @param code [String]
|
141
|
+
# @param sexp [SlimLint::Sexp]
|
142
|
+
def append(code, sexp)
|
143
|
+
raise "Unexpected newline!" if code.match?(/\n/)
|
144
|
+
|
145
|
+
@source_lines << code.dup
|
146
|
+
@line_count += 1
|
147
|
+
|
148
|
+
if code.empty?
|
149
|
+
@source_map[@line_count] = sexp.location
|
150
|
+
else
|
151
|
+
@source_lines.last.prepend(" " * @indent)
|
152
|
+
@source_map[@line_count] = sexp.location.adjust(column: -2 * @indent)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def append_dynamic(code, sexp)
|
157
|
+
return if code.empty?
|
158
|
+
@source_lines << "#{" " * @indent}p #{code}"
|
159
|
+
@line_count += 1
|
160
|
+
@source_map[@line_count] = sexp.location.adjust(column: (-2 * @indent) - 2)
|
161
|
+
end
|
162
|
+
|
163
|
+
def append_interpolated(code, sexp)
|
164
|
+
return if code.empty?
|
165
|
+
@source_lines << %(#{" " * @indent}p "x\#{#{code}}x")
|
166
|
+
@line_count += 1
|
167
|
+
@source_map[@line_count] = code.location.adjust(column: (-2 * @indent) - 6)
|
168
|
+
end
|
169
|
+
|
170
|
+
def append_dummy_puts(sexp)
|
171
|
+
append("_slim_lint_puts_#{@dummy_puts_count}", sexp)
|
172
|
+
@dummy_puts_count += 1
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubocop"
|
4
|
+
require "rubocop/ast/builder"
|
5
|
+
require "parser/current"
|
6
|
+
|
7
|
+
module SlimLint
|
8
|
+
# Parser for the Ruby language.
|
9
|
+
#
|
10
|
+
# This provides a convenient wrapper around the `parser` gem and the
|
11
|
+
# `astrolabe` integration to go with it. It is intended to be used for linter
|
12
|
+
# checks that require deep inspection of Ruby code.
|
13
|
+
class RubyParser
|
14
|
+
# Creates a reusable parser.
|
15
|
+
def initialize
|
16
|
+
@builder = ::RuboCop::AST::Builder.new
|
17
|
+
@parser = ::Parser::CurrentRuby.new(@builder)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parse the given Ruby source into an abstract syntax tree.
|
21
|
+
#
|
22
|
+
# @param source [String] Ruby source code
|
23
|
+
# @return [Array] syntax tree in the form returned by Parser gem
|
24
|
+
def parse(source)
|
25
|
+
buffer = ::Parser::Source::Buffer.new("(string)")
|
26
|
+
buffer.source = source
|
27
|
+
|
28
|
+
@parser.reset
|
29
|
+
@parser.parse(buffer)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Responsible for running the applicable linters against the desired files.
|
5
|
+
class Runner
|
6
|
+
# Runs the appropriate linters against the desired files given the specified
|
7
|
+
# options.
|
8
|
+
#
|
9
|
+
# @param [Hash] options
|
10
|
+
# @option options :config_file [String] path of configuration file to load
|
11
|
+
# @option options :config [SlimLint::Configuration] configuration to use
|
12
|
+
# @option options :excluded_files [Array<String>]
|
13
|
+
# @option options :included_linters [Array<String>]
|
14
|
+
# @option options :excluded_linters [Array<String>]
|
15
|
+
# @return [SlimLint::Report] a summary of all lints found
|
16
|
+
def run(options = {})
|
17
|
+
config = load_applicable_config(options)
|
18
|
+
linter_selector = SlimLint::LinterSelector.new(config, options)
|
19
|
+
|
20
|
+
if options[:stdin_file_path].nil?
|
21
|
+
files = extract_applicable_files(config, options)
|
22
|
+
lints = files.map do |file|
|
23
|
+
collect_lints(File.read(file), file, linter_selector, config)
|
24
|
+
end.flatten
|
25
|
+
else
|
26
|
+
files = [options[:stdin_file_path]]
|
27
|
+
lints = collect_lints($stdin.read, options[:stdin_file_path], linter_selector, config)
|
28
|
+
end
|
29
|
+
|
30
|
+
SlimLint::Report.new(lints, files)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Returns the {SlimLint::Configuration} that should be used given the
|
36
|
+
# specified options.
|
37
|
+
#
|
38
|
+
# @param options [Hash]
|
39
|
+
# @return [SlimLint::Configuration]
|
40
|
+
def load_applicable_config(options)
|
41
|
+
if options[:config_file]
|
42
|
+
SlimLint::ConfigurationLoader.load_file(options[:config_file])
|
43
|
+
elsif options[:config]
|
44
|
+
options[:config]
|
45
|
+
else
|
46
|
+
SlimLint::ConfigurationLoader.load_applicable_config
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Runs all provided linters using the specified config against the given
|
51
|
+
# file.
|
52
|
+
#
|
53
|
+
# @param file [String] path to file to lint
|
54
|
+
# @param linter_selector [SlimLint::LinterSelector]
|
55
|
+
# @param config [SlimLint::Configuration]
|
56
|
+
def collect_lints(file_content, file_name, linter_selector, config)
|
57
|
+
begin
|
58
|
+
document = SlimLint::Document.new(file_content, file: file_name, config: config)
|
59
|
+
rescue SlimLint::Exceptions::ParseError => e
|
60
|
+
return [SlimLint::Lint.new(nil, file_name, SourceLocation.new(start_line: e.lineno), e.error, :error)]
|
61
|
+
end
|
62
|
+
|
63
|
+
linter_selector.linters_for_file(file_name).map do |linter|
|
64
|
+
linter.run(document)
|
65
|
+
end.flatten
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the list of files that should be linted given the specified
|
69
|
+
# configuration and options.
|
70
|
+
#
|
71
|
+
# @param config [SlimLint::Configuration]
|
72
|
+
# @param options [Hash]
|
73
|
+
# @return [Array<String>]
|
74
|
+
def extract_applicable_files(config, options)
|
75
|
+
included_patterns = options[:files]
|
76
|
+
excluded_patterns = config["exclude"]
|
77
|
+
excluded_patterns += options.fetch(:excluded_files, [])
|
78
|
+
|
79
|
+
SlimLint::FileFinder.new(config).find(included_patterns, excluded_patterns)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Symbolic expression which represents tree-structured data.
|
5
|
+
#
|
6
|
+
# The main use of this particular implementation is to provide a single
|
7
|
+
# location for defining convenience helpers when operating on Sexps.
|
8
|
+
class Sexp < Array
|
9
|
+
# Stores the line number of the code in the original document that
|
10
|
+
# corresponds to this Sexp.
|
11
|
+
attr_accessor :start, :finish
|
12
|
+
|
13
|
+
def line
|
14
|
+
start[0] if start
|
15
|
+
end
|
16
|
+
|
17
|
+
def column
|
18
|
+
start[1] if start
|
19
|
+
end
|
20
|
+
|
21
|
+
# Creates an {Sexp} from the given {Array}-based Sexp.
|
22
|
+
#
|
23
|
+
# This provides a convenient way to convert between literal arrays of
|
24
|
+
# {Symbol}s and {Sexp}s containing {Atom}s and nested {Sexp}s. These objects
|
25
|
+
# all expose a similar API that conveniently allows the two objects to be
|
26
|
+
# treated similarly due to duck typing.
|
27
|
+
#
|
28
|
+
# @param array_sexp [Array]
|
29
|
+
def initialize(*array_sexp, start:, finish:)
|
30
|
+
@start = start
|
31
|
+
@finish = finish
|
32
|
+
array_sexp.each do |atom_or_sexp|
|
33
|
+
case atom_or_sexp
|
34
|
+
when Sexp, Atom
|
35
|
+
push atom_or_sexp
|
36
|
+
when Array
|
37
|
+
push Sexp.new(*atom_or_sexp, start: start, finish: finish)
|
38
|
+
else
|
39
|
+
push SlimLint::Atom.new(atom_or_sexp, pos: start)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def location
|
45
|
+
SourceLocation.new(
|
46
|
+
start_line: start[0],
|
47
|
+
start_column: start[1],
|
48
|
+
last_line: (finish || start)[0],
|
49
|
+
last_column: (finish || start)[1]
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns whether this {Sexp} matches the given Sexp pattern.
|
54
|
+
#
|
55
|
+
# A Sexp pattern is simply an incomplete Sexp prefix.
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# The following Sexp:
|
59
|
+
#
|
60
|
+
# [:html, :doctype, "html5"]
|
61
|
+
#
|
62
|
+
# ...will match the given patterns:
|
63
|
+
#
|
64
|
+
# [:html]
|
65
|
+
# [:html, :doctype]
|
66
|
+
# [:html, :doctype, "html5"]
|
67
|
+
#
|
68
|
+
# Note that nested Sexps will also be matched, so be careful about the cost
|
69
|
+
# of matching against a complicated pattern.
|
70
|
+
#
|
71
|
+
# @param sexp_pattern [Object,Array]
|
72
|
+
# @return [Boolean]
|
73
|
+
def match?(sexp_pattern)
|
74
|
+
# Delegate matching logic if we're comparing against a matcher
|
75
|
+
if sexp_pattern.is_a?(SlimLint::Matcher::Base)
|
76
|
+
return sexp_pattern.match?(self)
|
77
|
+
end
|
78
|
+
|
79
|
+
# If there aren't enough items to compare then this obviously won't match
|
80
|
+
return false unless sexp_pattern.is_a?(Array) && length >= sexp_pattern.length
|
81
|
+
|
82
|
+
sexp_pattern.each_with_index do |sub_pattern, index|
|
83
|
+
return false unless self[index].match?(sub_pattern)
|
84
|
+
end
|
85
|
+
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_array
|
90
|
+
map(&:to_array)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns pretty-printed representation of this S-expression.
|
94
|
+
#
|
95
|
+
# @return [String]
|
96
|
+
def inspect
|
97
|
+
display
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
# Pretty-prints this Sexp in a form that is more readable.
|
103
|
+
#
|
104
|
+
# @param depth [Integer] indentation level to display Sexp at
|
105
|
+
# @return [String]
|
106
|
+
def display(depth = 1)
|
107
|
+
indentation = " " * 2 * depth
|
108
|
+
range = +""
|
109
|
+
range << start.join(":") if start
|
110
|
+
range << " => " if start && finish
|
111
|
+
range << finish.join(":") if finish
|
112
|
+
output = "S(#{range})["
|
113
|
+
|
114
|
+
each_with_index do |nested_sexp, index|
|
115
|
+
output << "\n"
|
116
|
+
output += indentation
|
117
|
+
output +=
|
118
|
+
if nested_sexp.is_a?(SlimLint::Sexp)
|
119
|
+
nested_sexp.display(depth + 1)
|
120
|
+
else
|
121
|
+
nested_sexp.inspect
|
122
|
+
end
|
123
|
+
|
124
|
+
# Add trailing comma unless this is the last item
|
125
|
+
output += ", " if index < length - 1
|
126
|
+
end
|
127
|
+
|
128
|
+
output << "\n" << " " * 2 * (depth - 1) unless empty?
|
129
|
+
output << "]"
|
130
|
+
|
131
|
+
output
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|