slim_lint_standard 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|