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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +21 -0
  3. data/bin/slim-lint-standard +7 -0
  4. data/config/default.yml +109 -0
  5. data/lib/slim_lint/atom.rb +129 -0
  6. data/lib/slim_lint/capture_map.rb +19 -0
  7. data/lib/slim_lint/cli.rb +167 -0
  8. data/lib/slim_lint/configuration.rb +111 -0
  9. data/lib/slim_lint/configuration_loader.rb +86 -0
  10. data/lib/slim_lint/constants.rb +10 -0
  11. data/lib/slim_lint/document.rb +78 -0
  12. data/lib/slim_lint/engine.rb +41 -0
  13. data/lib/slim_lint/exceptions.rb +20 -0
  14. data/lib/slim_lint/file_finder.rb +88 -0
  15. data/lib/slim_lint/filter.rb +126 -0
  16. data/lib/slim_lint/filters/attribute_processor.rb +46 -0
  17. data/lib/slim_lint/filters/auto_indenter.rb +39 -0
  18. data/lib/slim_lint/filters/control_processor.rb +46 -0
  19. data/lib/slim_lint/filters/do_inserter.rb +39 -0
  20. data/lib/slim_lint/filters/end_inserter.rb +74 -0
  21. data/lib/slim_lint/filters/interpolation.rb +73 -0
  22. data/lib/slim_lint/filters/multi_flattener.rb +32 -0
  23. data/lib/slim_lint/filters/splat_processor.rb +20 -0
  24. data/lib/slim_lint/filters/static_merger.rb +47 -0
  25. data/lib/slim_lint/lint.rb +70 -0
  26. data/lib/slim_lint/linter/avoid_multiline_expressions.rb +41 -0
  27. data/lib/slim_lint/linter/comment_control_statement.rb +26 -0
  28. data/lib/slim_lint/linter/consecutive_control_statements.rb +26 -0
  29. data/lib/slim_lint/linter/control_statement_spacing.rb +32 -0
  30. data/lib/slim_lint/linter/dynamic_output_spacing.rb +77 -0
  31. data/lib/slim_lint/linter/embedded_engines.rb +18 -0
  32. data/lib/slim_lint/linter/empty_control_statement.rb +15 -0
  33. data/lib/slim_lint/linter/empty_lines.rb +24 -0
  34. data/lib/slim_lint/linter/file_length.rb +18 -0
  35. data/lib/slim_lint/linter/line_length.rb +18 -0
  36. data/lib/slim_lint/linter/redundant_div.rb +21 -0
  37. data/lib/slim_lint/linter/rubocop.rb +131 -0
  38. data/lib/slim_lint/linter/standard.rb +69 -0
  39. data/lib/slim_lint/linter/tab.rb +20 -0
  40. data/lib/slim_lint/linter/tag_case.rb +15 -0
  41. data/lib/slim_lint/linter/trailing_blank_lines.rb +19 -0
  42. data/lib/slim_lint/linter/trailing_whitespace.rb +17 -0
  43. data/lib/slim_lint/linter.rb +93 -0
  44. data/lib/slim_lint/linter_registry.rb +37 -0
  45. data/lib/slim_lint/linter_selector.rb +87 -0
  46. data/lib/slim_lint/logger.rb +103 -0
  47. data/lib/slim_lint/matcher/anything.rb +11 -0
  48. data/lib/slim_lint/matcher/base.rb +21 -0
  49. data/lib/slim_lint/matcher/capture.rb +32 -0
  50. data/lib/slim_lint/matcher/nothing.rb +13 -0
  51. data/lib/slim_lint/options.rb +110 -0
  52. data/lib/slim_lint/parser.rb +584 -0
  53. data/lib/slim_lint/rake_task.rb +125 -0
  54. data/lib/slim_lint/report.rb +25 -0
  55. data/lib/slim_lint/reporter/checkstyle_reporter.rb +42 -0
  56. data/lib/slim_lint/reporter/default_reporter.rb +40 -0
  57. data/lib/slim_lint/reporter/emacs_reporter.rb +40 -0
  58. data/lib/slim_lint/reporter/json_reporter.rb +50 -0
  59. data/lib/slim_lint/reporter.rb +44 -0
  60. data/lib/slim_lint/ruby_extract_engine.rb +30 -0
  61. data/lib/slim_lint/ruby_extractor.rb +175 -0
  62. data/lib/slim_lint/ruby_parser.rb +32 -0
  63. data/lib/slim_lint/runner.rb +82 -0
  64. data/lib/slim_lint/sexp.rb +134 -0
  65. data/lib/slim_lint/sexp_visitor.rb +150 -0
  66. data/lib/slim_lint/source_location.rb +45 -0
  67. data/lib/slim_lint/utils.rb +84 -0
  68. data/lib/slim_lint/version.rb +6 -0
  69. data/lib/slim_lint.rb +55 -0
  70. 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