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,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "slim_lint/ruby_extractor"
|
4
|
+
require "slim_lint/ruby_extract_engine"
|
5
|
+
|
6
|
+
module SlimLint
|
7
|
+
class Linter
|
8
|
+
# Runs RuboCop on Ruby code extracted from Slim templates.
|
9
|
+
class Standard < RuboCop
|
10
|
+
include LinterRegistry
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
require "standard"
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# Executes RuboCop against the given Ruby code and records the offenses as
|
20
|
+
# lints.
|
21
|
+
#
|
22
|
+
# @param ruby [String] Ruby code
|
23
|
+
# @param source_map [Hash] map of Ruby code line numbers to original line
|
24
|
+
# numbers in the template
|
25
|
+
def find_lints(ruby, source_map)
|
26
|
+
filename = document.file ? "#{document.file}.rb" : "ruby_script.rb"
|
27
|
+
|
28
|
+
with_ruby_from_stdin(ruby) do
|
29
|
+
extract_lints_from_offenses(lint_file(filename), source_map)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Defined so we can stub the results in tests
|
34
|
+
#
|
35
|
+
# @param file [String]
|
36
|
+
# @return [Array<RuboCop::Cop::Offense>]
|
37
|
+
def lint_file(filename)
|
38
|
+
::Standard::Cli.new(rubocop_flags << filename).run
|
39
|
+
OffenseCollector.offenses
|
40
|
+
end
|
41
|
+
|
42
|
+
# Aggregates RuboCop offenses and converts them to {SlimLint::Lint}s
|
43
|
+
# suitable for reporting.
|
44
|
+
#
|
45
|
+
# @param offenses [Array<RuboCop::Cop::Offense>]
|
46
|
+
# @param source_map [Hash]
|
47
|
+
def extract_lints_from_offenses(offenses, source_map)
|
48
|
+
offenses.each do |offense|
|
49
|
+
@lints << Lint.new(
|
50
|
+
[self, offense.cop_name],
|
51
|
+
document.file,
|
52
|
+
location_for_line(source_map, offense),
|
53
|
+
offense.message.gsub(/ at \d+, \d+/, "")
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns flags that will be passed to RuboCop CLI.
|
59
|
+
#
|
60
|
+
# @return [Array<String>]
|
61
|
+
def rubocop_flags
|
62
|
+
flags = %w[--format SlimLint::Linter::RuboCop::OffenseCollector]
|
63
|
+
flags += ["--no-display-cop-names"]
|
64
|
+
flags += ["--stdin"]
|
65
|
+
flags
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Searches for tab indentation
|
5
|
+
class Linter::Tab < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
TAB_RE = /^( *)[\t ]*\t/
|
9
|
+
MSG = "Tab detected"
|
10
|
+
|
11
|
+
on_start do |_sexp|
|
12
|
+
document.source_lines.each.with_index(1) do |line, lineno|
|
13
|
+
next unless TAB_RE.match?(line)
|
14
|
+
|
15
|
+
sexp = Sexp.new(:dummy, start: [lineno, 0], finish: [lineno, ($` ? $`.size : 0)])
|
16
|
+
report_lint(sexp, MSG)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Searches for tags with uppercase characters.
|
5
|
+
class Linter::TagCase < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
on [:html, :tag] do |sexp|
|
9
|
+
_, _, name = sexp
|
10
|
+
next unless name[/[A-Z]/]
|
11
|
+
|
12
|
+
report_lint(sexp, "Tag `#{name}` should be written as `#{name.downcase}`")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# This linter looks for trailing blank lines and a final newline.
|
5
|
+
class Linter::TrailingBlankLines < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
on_start do |_sexp|
|
9
|
+
next if document.source.empty?
|
10
|
+
|
11
|
+
sexp = Sexp.new(:dummy, start: [document.source.lines.size, 0], finish: [document.source.lines.size, 0])
|
12
|
+
if !document.source.end_with?("\n")
|
13
|
+
report_lint(sexp, "No blank line in the end of file")
|
14
|
+
elsif document.source.lines.last.blank?
|
15
|
+
report_lint(sexp, "Multiple empty lines in the end of file")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Checks for trailing whitespace.
|
5
|
+
class Linter::TrailingWhitespace < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
on_start do |_sexp|
|
9
|
+
document.source_lines.each.with_index(1) do |line, lineno|
|
10
|
+
next unless /\s+$/.match?(line)
|
11
|
+
|
12
|
+
sexp = Sexp.new(:dummy, start: [lineno, line.rstrip.size], finish: [lineno, line.size])
|
13
|
+
report_lint(sexp, "Line contains trailing whitespace")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Base implementation for all lint checks.
|
5
|
+
#
|
6
|
+
# @abstract
|
7
|
+
class Linter
|
8
|
+
# Include definitions for Sexp pattern-matching helpers.
|
9
|
+
include SexpVisitor
|
10
|
+
extend SexpVisitor::DSL
|
11
|
+
|
12
|
+
# List of lints reported by this linter.
|
13
|
+
#
|
14
|
+
# @todo Remove once spec/support/shared_linter_context returns an array of
|
15
|
+
# lints for the subject instead of the linter itself.
|
16
|
+
attr_reader :lints
|
17
|
+
|
18
|
+
# Initializes a linter with the specified configuration.
|
19
|
+
#
|
20
|
+
# @param config [Hash] configuration for this linter
|
21
|
+
def initialize(config)
|
22
|
+
@config = config
|
23
|
+
@lints = []
|
24
|
+
end
|
25
|
+
|
26
|
+
# Runs the linter against the given Slim document.
|
27
|
+
#
|
28
|
+
# @param document [SlimLint::Document]
|
29
|
+
def run(document)
|
30
|
+
@document = document
|
31
|
+
@lints = []
|
32
|
+
@disabled_lines = nil
|
33
|
+
trigger_pattern_callbacks(document.sexp)
|
34
|
+
@lints
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the simple name for this linter.
|
38
|
+
#
|
39
|
+
# @return [String]
|
40
|
+
def name
|
41
|
+
self.class.name.split("::").last
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :config, :document
|
47
|
+
|
48
|
+
# Record a lint for reporting back to the user.
|
49
|
+
#
|
50
|
+
# @param node [Sexp, Atom] node to extract the line number from
|
51
|
+
# @param message [String] error/warning to display to the user
|
52
|
+
def report_lint(node, message)
|
53
|
+
return if disabled_for_line?(node.line)
|
54
|
+
|
55
|
+
@lints << SlimLint::Lint.new(self, @document.file, node.location, message)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Parse Ruby code into an abstract syntax tree.
|
59
|
+
#
|
60
|
+
# @param source [String] Ruby code to parse
|
61
|
+
# @return [AST::Node]
|
62
|
+
def parse_ruby(source)
|
63
|
+
@ruby_parser ||= SlimLint::RubyParser.new
|
64
|
+
@ruby_parser.parse(source)
|
65
|
+
end
|
66
|
+
|
67
|
+
def disabled_for_line?(line)
|
68
|
+
disabled_lines.include?(line)
|
69
|
+
end
|
70
|
+
|
71
|
+
def disabled_lines
|
72
|
+
@disabled_lines ||= begin
|
73
|
+
currently_disabled = false
|
74
|
+
@document.source_lines.each_with_index.each_with_object([]) do |pair, lines|
|
75
|
+
line = pair[0]
|
76
|
+
line_number = pair[1] + 1
|
77
|
+
|
78
|
+
if line.match?(%r{/ slim-lint:disable #{linter_name}})
|
79
|
+
currently_disabled = true
|
80
|
+
elsif line.match?(%r{/ slim-lint:enable #{linter_name}})
|
81
|
+
currently_disabled = false
|
82
|
+
elsif currently_disabled
|
83
|
+
lines << line_number
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def linter_name
|
90
|
+
@linter_name ||= self.class.name.split("::").last
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
class NoSuchLinter < StandardError; end
|
5
|
+
|
6
|
+
# Stores all defined linters.
|
7
|
+
module LinterRegistry
|
8
|
+
@linters = []
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# List of all registered linters.
|
12
|
+
attr_reader :linters
|
13
|
+
|
14
|
+
# Executed when a linter includes the {LinterRegistry} module.
|
15
|
+
#
|
16
|
+
# This results in the linter being registered with the registry.
|
17
|
+
#
|
18
|
+
# @param subclass [Class]
|
19
|
+
def included(subclass)
|
20
|
+
@linters << subclass
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return a list of {SlimLint::Linter} {Class}es corresponding to the
|
24
|
+
# specified list of names.
|
25
|
+
#
|
26
|
+
# @param linter_names [Array<String>]
|
27
|
+
# @return [Array<Class>]
|
28
|
+
def extract_linters_from(linter_names)
|
29
|
+
linter_names.map do |linter_name|
|
30
|
+
SlimLint::Linter.const_get(linter_name)
|
31
|
+
rescue NameError
|
32
|
+
raise NoSuchLinter, "Linter #{linter_name} does not exist"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Chooses the appropriate linters to run given the specified configuration.
|
5
|
+
class LinterSelector
|
6
|
+
# Creates a selector using the given configuration and additional options.
|
7
|
+
#
|
8
|
+
# @param config [SlimLint::Configuration]
|
9
|
+
# @param options [Hash]
|
10
|
+
def initialize(config, options)
|
11
|
+
@config = config
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the set of linters to run against the given file.
|
16
|
+
#
|
17
|
+
# @param file [String]
|
18
|
+
# @raise [SlimLint::Exceptions::NoLintersError] when no linters are enabled
|
19
|
+
# @return [Array<SlimLint::Linter>]
|
20
|
+
def linters_for_file(file)
|
21
|
+
@linters ||= extract_enabled_linters(@config, @options)
|
22
|
+
@linters.select { |linter| run_linter_on_file?(@config, linter, file) }
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Returns a list of linter names that are enabled given the specified
|
28
|
+
# configuration and additional options.
|
29
|
+
#
|
30
|
+
# @param config [SlimLint::Configuration]
|
31
|
+
# @param options [Hash]
|
32
|
+
# @return [Array<String>]
|
33
|
+
def extract_enabled_linter_names(config, options)
|
34
|
+
included_linters = options.fetch(:included_linters, [])
|
35
|
+
included_linters = LinterRegistry.linters.map(&:name) if included_linters.empty?
|
36
|
+
|
37
|
+
excluded_linters = options.fetch(:excluded_linters, [])
|
38
|
+
|
39
|
+
# After filtering out explicitly included/excluded linters, only include
|
40
|
+
# linters which are enabled in the configuration
|
41
|
+
linters = (included_linters - excluded_linters).select do |name|
|
42
|
+
config.for_linter(name)["enabled"]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Highlight condition where all linters were filtered out, as this was
|
46
|
+
# likely a mistake on the user's part
|
47
|
+
if linters.empty?
|
48
|
+
raise SlimLint::Exceptions::NoLintersError, "No linters specified"
|
49
|
+
end
|
50
|
+
|
51
|
+
linters
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns a list of linters that are enabled given the specified
|
55
|
+
# configuration and additional options.
|
56
|
+
#
|
57
|
+
# @param config [SlimLint::Configuration]
|
58
|
+
# @param options [Hash]
|
59
|
+
# @return [Array<SlimLint::Linter>]
|
60
|
+
def extract_enabled_linters(config, options)
|
61
|
+
linter_names = extract_enabled_linter_names(config, options)
|
62
|
+
linter_classes = LinterRegistry.extract_linters_from(linter_names)
|
63
|
+
linter_classes.map { |klass| klass.new(config.for_linter(klass)) }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Whether to run the given linter against the specified file.
|
67
|
+
#
|
68
|
+
# @param config [SlimLint::Configuration]
|
69
|
+
# @param linter [SlimLint::Linter]
|
70
|
+
# @param file [String]
|
71
|
+
# @return [Boolean]
|
72
|
+
def run_linter_on_file?(config, linter, file)
|
73
|
+
linter_config = config.for_linter(linter)
|
74
|
+
incl, excl = linter_config["include"], linter_config["exclude"]
|
75
|
+
|
76
|
+
if incl.any? && !SlimLint::Utils.any_glob_matches?(incl, file)
|
77
|
+
return false
|
78
|
+
end
|
79
|
+
|
80
|
+
if SlimLint::Utils.any_glob_matches?(excl, file)
|
81
|
+
return false
|
82
|
+
end
|
83
|
+
|
84
|
+
true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint
|
4
|
+
# Encapsulates all communication to an output source.
|
5
|
+
class Logger
|
6
|
+
# Whether colored output via ANSI escape sequences is enabled.
|
7
|
+
# @return [true,false]
|
8
|
+
attr_accessor :color_enabled
|
9
|
+
|
10
|
+
# Creates a logger which outputs nothing.
|
11
|
+
# @return [SlimLint::Logger]
|
12
|
+
def self.silent
|
13
|
+
new(File.open("/dev/null", "w"))
|
14
|
+
end
|
15
|
+
|
16
|
+
# Creates a new {SlimLint::Logger} instance.
|
17
|
+
#
|
18
|
+
# @param out [IO] the output destination.
|
19
|
+
def initialize(out)
|
20
|
+
@out = out
|
21
|
+
end
|
22
|
+
|
23
|
+
# Print the specified output.
|
24
|
+
#
|
25
|
+
# @param output [String] the output to send
|
26
|
+
# @param newline [true,false] whether to append a newline
|
27
|
+
def log(output, newline = true)
|
28
|
+
@out.print(output)
|
29
|
+
@out.print("\n") if newline
|
30
|
+
end
|
31
|
+
|
32
|
+
# Print the specified output in bold face.
|
33
|
+
# If output destination is not a TTY, behaves the same as {#log}.
|
34
|
+
#
|
35
|
+
# @param args [Array<String>]
|
36
|
+
def bold(*args)
|
37
|
+
color("1", *args)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Print the specified output in a color indicative of error.
|
41
|
+
# If output destination is not a TTY, behaves the same as {#log}.
|
42
|
+
#
|
43
|
+
# @param args [Array<String>]
|
44
|
+
def error(*args)
|
45
|
+
color(31, *args)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Print the specified output in a bold face and color indicative of error.
|
49
|
+
# If output destination is not a TTY, behaves the same as {#log}.
|
50
|
+
#
|
51
|
+
# @param args [Array<String>]
|
52
|
+
def bold_error(*args)
|
53
|
+
color("1;31", *args)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Print the specified output in a color indicative of success.
|
57
|
+
# If output destination is not a TTY, behaves the same as {#log}.
|
58
|
+
#
|
59
|
+
# @param args [Array<String>]
|
60
|
+
def success(*args)
|
61
|
+
color(32, *args)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Print the specified output in a color indicative of a warning.
|
65
|
+
# If output destination is not a TTY, behaves the same as {#log}.
|
66
|
+
#
|
67
|
+
# @param args [Array<String>]
|
68
|
+
def warning(*args)
|
69
|
+
color(33, *args)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Print the specified output in a color indicating information.
|
73
|
+
# If output destination is not a TTY, behaves the same as {#log}.
|
74
|
+
#
|
75
|
+
# @param args [Array<String>]
|
76
|
+
def info(*args)
|
77
|
+
color(36, *args)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Print a blank line.
|
81
|
+
def newline
|
82
|
+
log("")
|
83
|
+
end
|
84
|
+
|
85
|
+
# Whether this logger is outputting to a TTY.
|
86
|
+
#
|
87
|
+
# @return [true,false]
|
88
|
+
def tty?
|
89
|
+
@out.respond_to?(:tty?) && @out.tty?
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# Print output in the specified color.
|
95
|
+
#
|
96
|
+
# @param code [Integer,String] ANSI color code
|
97
|
+
# @param output [String] output to print
|
98
|
+
# @param newline [Boolean] whether to append a newline
|
99
|
+
def color(code, output, newline = true)
|
100
|
+
log(color_enabled ? "\033[#{code}m#{output}\033[0m" : output, newline)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint::Matcher
|
4
|
+
# Represents a Sexp pattern implementing complex matching logic.
|
5
|
+
#
|
6
|
+
# Subclasses can implement custom logic to create complex matches that can be
|
7
|
+
# reused across linters, DRYing up matching code.
|
8
|
+
#
|
9
|
+
# @abstract
|
10
|
+
class Base
|
11
|
+
# Whether this matcher matches the specified object.
|
12
|
+
#
|
13
|
+
# This must be implemented by subclasses.
|
14
|
+
#
|
15
|
+
# @param other [Object]
|
16
|
+
# @return [Boolean]
|
17
|
+
def match?(*)
|
18
|
+
raise NotImplementedError, "Matcher must implement `match?`"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlimLint::Matcher
|
4
|
+
# Wraps a matcher, taking on the behavior of the wrapped matcher but storing
|
5
|
+
# the value that matched so it can be referred to later.
|
6
|
+
class Capture < Base
|
7
|
+
# @return [SlimLint::Matcher::Base] matcher that this capture wraps
|
8
|
+
attr_accessor :matcher
|
9
|
+
|
10
|
+
# @return [Object] value that was captured
|
11
|
+
attr_accessor :value
|
12
|
+
|
13
|
+
# Creates a capture that wraps that given matcher.
|
14
|
+
#
|
15
|
+
# @param matcher [SlimLint::Matcher::Base]
|
16
|
+
# @return [SlimLint::Matcher::Capture]
|
17
|
+
def self.from_matcher(matcher)
|
18
|
+
new.tap do |cap_matcher|
|
19
|
+
cap_matcher.matcher = matcher
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# @see {SlimLint::Matcher::Base#match?}
|
24
|
+
def match?(object)
|
25
|
+
if (result = @matcher.match?(object))
|
26
|
+
@value = object
|
27
|
+
end
|
28
|
+
|
29
|
+
result
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
|
5
|
+
module SlimLint
|
6
|
+
# Handles option parsing for the command line application.
|
7
|
+
class Options
|
8
|
+
# Parses command line options into an options hash.
|
9
|
+
#
|
10
|
+
# @param args [Array<String>] arguments passed via the command line
|
11
|
+
# @return [Hash] parsed options
|
12
|
+
def parse(args)
|
13
|
+
@options = {}
|
14
|
+
|
15
|
+
OptionParser.new do |parser|
|
16
|
+
parser.banner = "Usage: #{APP_NAME} [options] [file1, file2, ...]"
|
17
|
+
|
18
|
+
add_linter_options parser
|
19
|
+
add_file_options parser
|
20
|
+
add_info_options parser
|
21
|
+
end.parse!(args)
|
22
|
+
|
23
|
+
# Any remaining arguments are assumed to be files
|
24
|
+
@options[:files] = args
|
25
|
+
|
26
|
+
@options
|
27
|
+
rescue OptionParser::InvalidOption => e
|
28
|
+
raise Exceptions::InvalidCLIOption,
|
29
|
+
e.message,
|
30
|
+
e.backtrace
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Register linter-related flags.
|
36
|
+
def add_linter_options(parser)
|
37
|
+
msg = "Specify which linters you want to run"
|
38
|
+
parser.on("-i", "--include-linter linter,...", Array, msg) do |linters|
|
39
|
+
@options[:included_linters] = linters
|
40
|
+
end
|
41
|
+
|
42
|
+
msg = "Specify which linters you don't want to run"
|
43
|
+
parser.on("-x", "--exclude-linter linter,...", Array, msg) do |linters|
|
44
|
+
@options[:excluded_linters] = linters
|
45
|
+
end
|
46
|
+
|
47
|
+
msg = "Specify which reporter you want to use to generate the output"
|
48
|
+
parser.on("-r", "--reporter reporter", String, msg) do |reporter|
|
49
|
+
@options[:reporter] = load_reporter_class(reporter.capitalize)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the class of the specified Reporter.
|
54
|
+
#
|
55
|
+
# @param reporter_name [String]
|
56
|
+
# @raise [SlimLint::Exceptions::InvalidCLIOption] if reporter doesn't exist
|
57
|
+
# @return [Class]
|
58
|
+
def load_reporter_class(reporter_name)
|
59
|
+
SlimLint::Reporter.const_get("#{reporter_name}Reporter")
|
60
|
+
rescue NameError
|
61
|
+
raise SlimLint::Exceptions::InvalidCLIOption,
|
62
|
+
"#{reporter_name}Reporter does not exist"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Register file-related flags.
|
66
|
+
def add_file_options(parser)
|
67
|
+
msg = "Specify which configuration file you want to use"
|
68
|
+
parser.on("-c", "--config config-file", String, msg) do |conf_file|
|
69
|
+
@options[:config_file] = conf_file
|
70
|
+
end
|
71
|
+
|
72
|
+
msg = "List of file names to exclude"
|
73
|
+
parser.on("-e", "--exclude file,...", Array, msg) do |files|
|
74
|
+
@options[:excluded_files] = files
|
75
|
+
end
|
76
|
+
|
77
|
+
msg = "Pipe source from STDIN, using file in offense reports."
|
78
|
+
parser.on("--stdin-file-path file", String, msg) do |file|
|
79
|
+
@options[:stdin_file_path] = file
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Register informational flags.
|
84
|
+
def add_info_options(parser)
|
85
|
+
parser.on("--show-linters", "Display available linters") do
|
86
|
+
@options[:show_linters] = true
|
87
|
+
end
|
88
|
+
|
89
|
+
parser.on("--show-reporters", "Display available reporters") do
|
90
|
+
@options[:show_reporters] = true
|
91
|
+
end
|
92
|
+
|
93
|
+
parser.on("--[no-]color", "Force output to be colorized") do |color|
|
94
|
+
@options[:color] = color
|
95
|
+
end
|
96
|
+
|
97
|
+
parser.on_tail("-h", "--help", "Display help documentation") do
|
98
|
+
@options[:help] = parser.help
|
99
|
+
end
|
100
|
+
|
101
|
+
parser.on_tail("-v", "--version", "Display version") do
|
102
|
+
@options[:version] = true
|
103
|
+
end
|
104
|
+
|
105
|
+
parser.on_tail("-V", "--verbose-version", "Display verbose version information") do
|
106
|
+
@options[:verbose_version] = true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|