slim_lint_standard 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,32 @@
1
+ module SlimLint
2
+ module Filters
3
+ # Flattens nested multi expressions while respecting source locatoins.
4
+ #
5
+ # @api public
6
+ class MultiFlattener < Filter
7
+ def on_slim_embedded(*args)
8
+ @self
9
+ end
10
+
11
+ def on_multi(*exps)
12
+ # If the multi contains a single element, just return the element
13
+ return compile(exps.first) if exps.size == 1
14
+
15
+ result = @self
16
+ result.clear
17
+ result.concat(@key)
18
+
19
+ exps.each do |exp|
20
+ exp = compile(exp)
21
+ if exp.first == :multi
22
+ result.concat(exp[1..])
23
+ else
24
+ result << exp
25
+ end
26
+ end
27
+
28
+ result
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ module Filters
5
+ # A dumbed-down version of {Slim::Splat::Filter} which doesn't introduced
6
+ # temporary variables or other cruft.
7
+ class SplatProcessor < Filter
8
+ # Handle slim splat expressions `[:slim, :splat, code]`
9
+ #
10
+ # @param code [String]
11
+ # @return [Array]
12
+ def on_slim_splat(code)
13
+ return code if code[0] == :multi
14
+ @self.delete_at(1)
15
+ @self.first.value = :code
16
+ @self
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,47 @@
1
+ module SlimLint
2
+ module Filters
3
+ # Merges several statics into a single static while respecting source
4
+ # location data. Example:
5
+ #
6
+ # [:multi,
7
+ # [:static, "Hello "],
8
+ # [:static, "World!"]]
9
+ #
10
+ # Compiles to:
11
+ #
12
+ # [:static, "Hello World!"]
13
+ #
14
+ # @api public
15
+ class StaticMerger < Filter
16
+ def on_slim_embedded(*exps)
17
+ @self
18
+ end
19
+
20
+ def on_multi(*exps)
21
+ result = @self
22
+ result.clear
23
+ result.concat(@key)
24
+
25
+ static = nil
26
+ exps.each do |exp|
27
+ if exp.first == :static
28
+ if static
29
+ static.finish = exp.finish if later_pos?(static.finish, exp.finish)
30
+ static.last.finish = exp.finish if later_pos?(static.last.finish, exp.finish)
31
+ static.last.value << exp.last.value
32
+ else
33
+ static = exp
34
+ static[1] = exp.last.dup
35
+ result << static
36
+ end
37
+ else
38
+ result << compile(exp)
39
+ static = nil unless exp.first == :newline
40
+ end
41
+ end
42
+
43
+ result.size == 2 ? result[1] : result
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Contains information about a problem or issue with a Slim document.
5
+ class Lint
6
+ # @return [String] file path to which the lint applies
7
+ attr_reader :filename
8
+
9
+ # @return [SourceLocation] location in the file the lint corresponds to
10
+ attr_reader :location
11
+
12
+ # @return [SlimLint::Linter] linter that reported the lint
13
+ attr_reader :linter
14
+
15
+ # @return [String] sublinter that reported the lint
16
+ attr_reader :sublinter
17
+
18
+ # @return [String] error/warning message to display to user
19
+ attr_reader :message
20
+
21
+ # @return [Symbol] whether this lint is a warning or an error
22
+ attr_reader :severity
23
+
24
+ # Creates a new lint.
25
+ #
26
+ # @param linter [SlimLint::Linter]
27
+ # @param filename [String]
28
+ # @param location [SourceLocation]
29
+ # @param message [String]
30
+ # @param severity [Symbol]
31
+ def initialize(linter, filename, location, message, severity = :warning)
32
+ @linter, @sublinter = Array(linter)
33
+ @filename = filename
34
+ @location = location
35
+ @message = message
36
+ @severity = severity
37
+ end
38
+
39
+ def line
40
+ location.line
41
+ end
42
+
43
+ def column
44
+ location.column
45
+ end
46
+
47
+ def last_line
48
+ location.last_line
49
+ end
50
+
51
+ def last_column
52
+ location.last_column
53
+ end
54
+
55
+ def cop
56
+ @sublinter || @linter.name if @linter
57
+ end
58
+
59
+ def name
60
+ [@linter.name, @sublinter].compact.join("/") if @linter
61
+ end
62
+
63
+ # Return whether this lint has a severity of error.
64
+ #
65
+ # @return [Boolean]
66
+ def error?
67
+ @severity == :error
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Searches for multi-line control statements, dynamic output statements,
5
+ # attribute values, and splats.
6
+ class Linter::AvoidMultilineExpressions < Linter
7
+ include LinterRegistry
8
+
9
+ on [:slim, :control] do |sexp|
10
+ _, _, code = sexp
11
+ next unless code.size > 2
12
+
13
+ msg = "Avoid control statements that span multiple lines."
14
+ report_lint(sexp, msg)
15
+ end
16
+
17
+ on [:slim, :output] do |sexp|
18
+ _, _, _, code = sexp
19
+ next unless code.size > 2
20
+
21
+ msg = "Avoid dynamic output statements that span multiple lines."
22
+ report_lint(sexp, msg)
23
+ end
24
+
25
+ on [:slim, :attrvalue] do |sexp|
26
+ _, _, _, code = sexp
27
+ next unless code.size > 2
28
+
29
+ msg = "Avoid attribute values that span multiple lines."
30
+ report_lint(sexp, msg)
31
+ end
32
+
33
+ on [:slim, :splat] do |sexp|
34
+ _, _, code = sexp
35
+ next unless code.size > 2
36
+
37
+ msg = "Avoid attribute values that span multiple lines."
38
+ report_lint(sexp, msg)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Searches for control statements with only comments.
5
+ class Linter::CommentControlStatement < Linter
6
+ include LinterRegistry
7
+
8
+ RUBOCOP_CONTROL_COMMENT_RE = /^\s*(rubocop|standard):\w+/
9
+ TEMPLATE_DEPENDENCY_CONTROL_COMMENT_RE = /^\s*Template Dependency:/
10
+
11
+ on [:slim, :control] do |sexp|
12
+ _, _, code = sexp
13
+ next unless code.last[1][/\A\s*#/]
14
+
15
+ comment = code.last[1][/\A\s*#(.*\z)/, 1]
16
+
17
+ next if RUBOCOP_CONTROL_COMMENT_RE.match?(comment)
18
+ next if TEMPLATE_DEPENDENCY_CONTROL_COMMENT_RE.match?(comment)
19
+
20
+ msg =
21
+ "Slim code comments (`/#{comment}`) are preferred over " \
22
+ "control statement comments (`-##{comment}`)"
23
+ report_lint(sexp, msg)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Searches for more than an allowed number of consecutive control code
5
+ # statements that could be condensed into a :ruby filter.
6
+ class Linter::ConsecutiveControlStatements < Linter
7
+ include LinterRegistry
8
+
9
+ on [:multi] do |sexp|
10
+ max = config["max_consecutive"] + 1
11
+ Utils.for_consecutive_items(sexp, method(:flat_control_statement?), max) do |group|
12
+ report_lint(
13
+ group.first,
14
+ "#{group.count} consecutive control statements can be " \
15
+ "merged into a single `ruby:` filter"
16
+ )
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def flat_control_statement?(sexp)
23
+ sexp.match?([:slim, :control]) && sexp[3] == [:multi]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Checks for missing or superfluous spacing before and after control statements.
5
+ class Linter::ControlStatementSpacing < Linter
6
+ include LinterRegistry
7
+
8
+ on [:slim, :control] do |sexp|
9
+ expr = sexp.last[0]
10
+ expr_line, expr_col = sexp.start
11
+ line = document.source_lines[expr_line - 1][(expr_col - 1)..]
12
+ after_pattern, after_action = after_config
13
+
14
+ unless line.match?(after_pattern)
15
+ report_lint(expr, "Please #{after_action} the dash")
16
+ end
17
+ end
18
+
19
+ def after_config
20
+ @after_config ||= case config["space_after"]
21
+ when "never", false, nil
22
+ [/^ *-#?[^# ]/, "remove spaces after"]
23
+ when "always", "single", true
24
+ [/^ *-#? [^ ]/, "use one space after"]
25
+ when "ignore", "any"
26
+ [//, ""]
27
+ else
28
+ raise ArgumentError, "Unknown value for `space_after`; please use 'never' or 'always'"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Checks for missing or superfluous spacing before and after dynamic tag output indicators.
5
+ class Linter::DynamicOutputSpacing < Linter
6
+ include LinterRegistry
7
+
8
+ PATTERN = "==?['<>]*"
9
+
10
+ on [:html, :tag, anything, [], capture(:expr, [:slim, :output, anything, anything])] do |sexp|
11
+ # Fetch original Slim code that contains an element with a control statement.
12
+ expr_line, expr_col = captures[:expr].start
13
+ line = document.source_lines[expr_line - 1][(expr_col - 1)..]
14
+
15
+ before_pattern, _ = before_config
16
+ after_pattern, _ = after_config
17
+
18
+ report(captures[:expr], line.match?(before_pattern), line.match?(after_pattern))
19
+
20
+ # Visit any children of the HTML tag, but don't _revisit_ this Slim output.
21
+ traverse_children(captures[:expr].last)
22
+ :stop
23
+ end
24
+
25
+ on [:slim, :output] do |sexp|
26
+ expr_line, expr_col = sexp.start
27
+ line = document.source_lines[expr_line - 1][(expr_col - 1)..]
28
+ after_pattern, _ = after_config
29
+
30
+ report(sexp, true, line.match?(after_pattern))
31
+ end
32
+
33
+ def report(expr, *results)
34
+ _, before_action = before_config
35
+ _, after_action = after_config
36
+
37
+ case results
38
+ when [false, true]
39
+ report_lint(expr, "Please #{before_action} the equals sign")
40
+ when [true, false]
41
+ report_lint(expr, "Please #{after_action} the equals sign")
42
+ when [false, false]
43
+ if before_action[0] == after_action[0]
44
+ report_lint(expr, "Please #{before_action} and after the equals sign")
45
+ else
46
+ report_lint(expr, "Please #{before_action} and #{after_action} the equals sign")
47
+ end
48
+ end
49
+ end
50
+
51
+ def before_config
52
+ @before_config ||= case config["space_before"]
53
+ when "never", false, nil
54
+ [/^#{PATTERN}/, "remove spaces before"]
55
+ when "always", "single", true
56
+ [/^ #{PATTERN}/, "use one space before"]
57
+ when "ignore", "any"
58
+ [//, ""]
59
+ else
60
+ raise ArgumentError, "Unknown value for `space_before`; please use 'never', 'always', or 'ignore'"
61
+ end
62
+ end
63
+
64
+ def after_config
65
+ @after_config ||= case config["space_after"]
66
+ when "never", false, nil
67
+ [/^ *#{PATTERN}[^ ]/, "remove spaces after"]
68
+ when "always", "single", true
69
+ [/^ *#{PATTERN} [^ ]/, "use one space after"]
70
+ when "ignore", "any"
71
+ [//, ""]
72
+ else
73
+ raise ArgumentError, "Unknown value for `space_after`; please use 'never', 'always', or 'ignore'"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Checks for forbidden embedded engines.
5
+ class Linter::EmbeddedEngines < Linter
6
+ include LinterRegistry
7
+
8
+ MESSAGE = "Forbidden embedded engine `%s` found"
9
+
10
+ on [:slim, :embedded] do |sexp|
11
+ _, _, engine, _ = sexp
12
+
13
+ forbidden_engines = config["forbidden_engines"]
14
+ next unless forbidden_engines.include?(engine)
15
+ report_lint(sexp, MESSAGE % engine)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Searches for control statements with no code.
5
+ class Linter::EmptyControlStatement < Linter
6
+ include LinterRegistry
7
+
8
+ on [:slim, :control] do |sexp|
9
+ _, _, code = sexp
10
+ next unless code.last[1][/\A\s*\Z/]
11
+
12
+ report_lint(sexp, "Empty control statement can be removed")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # This linter checks for two or more consecutive blank lines
5
+ # and for the first blank line in file.
6
+ class Linter::EmptyLines < Linter
7
+ include LinterRegistry
8
+
9
+ on_start do |_sexp|
10
+ was_empty = true
11
+ document.source.lines.each.with_index(1) do |line, i|
12
+ if line.blank?
13
+ if was_empty
14
+ sexp = Sexp.new(start: [i, 0], finish: [i, 0])
15
+ report_lint(sexp, "Extra empty line detected")
16
+ end
17
+ was_empty = true
18
+ else
19
+ was_empty = false
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Checks for file longer than a maximum number of lines.
5
+ class Linter::FileLength < Linter
6
+ include LinterRegistry
7
+
8
+ MSG = "File is too long. [%d/%d]"
9
+
10
+ on_start do |_sexp|
11
+ count = document.source_lines.size
12
+ if count > config["max"]
13
+ sexp = Sexp.new(start: [1, 0], finish: [1, 0])
14
+ report_lint(sexp, format(MSG, count, config["max"]))
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Checks for lines longer than a maximum number of columns.
5
+ class Linter::LineLength < Linter
6
+ include LinterRegistry
7
+
8
+ MSG = "Line is too long. [%d/%d]"
9
+
10
+ on_start do |_sexp|
11
+ document.source_lines.each.with_index(1) do |line, i|
12
+ next if line.length <= config["max"]
13
+ sexp = Sexp.new(start: [i, 0], finish: [i, 0])
14
+ report_lint(sexp, format(MSG, line.length, config["max"]))
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Checks for unnecessary uses of the `div` tag where a class name or ID
5
+ # already implies a div.
6
+ class Linter::RedundantDiv < Linter
7
+ include LinterRegistry
8
+
9
+ SHORTCUT_ATTRS = %w[id class]
10
+ MESSAGE = "`div` is redundant when %s attribute shortcut is present"
11
+
12
+ on [:html, :tag, "div", capture(:attrs, [:html, :attrs]), anything] do |sexp|
13
+ _, _, name, value = captures[:attrs][2]
14
+ next unless name
15
+ next unless value[0] == :static
16
+ next unless SHORTCUT_ATTRS.include?(name.value)
17
+
18
+ report_lint(sexp[2], MESSAGE % name)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "slim_lint/ruby_extractor"
4
+ require "slim_lint/ruby_extract_engine"
5
+ require "rubocop"
6
+
7
+ module SlimLint
8
+ class Linter
9
+ # Runs RuboCop on Ruby code extracted from Slim templates.
10
+ class RuboCop < Linter
11
+ include LinterRegistry
12
+
13
+ on_start do |_sexp|
14
+ processed_sexp = SlimLint::RubyExtractEngine.new.call(document.source)
15
+
16
+ extractor = SlimLint::RubyExtractor.new
17
+ extracted_source = extractor.extract(processed_sexp)
18
+
19
+ next if extracted_source.source.empty?
20
+
21
+ find_lints(extracted_source.source, extracted_source.source_map)
22
+ end
23
+
24
+ private
25
+
26
+ # Executes RuboCop against the given Ruby code and records the offenses as
27
+ # lints.
28
+ #
29
+ # @param ruby [String] Ruby code
30
+ # @param source_map [Hash] map of Ruby code line numbers to original line
31
+ # numbers in the template
32
+ def find_lints(ruby, source_map)
33
+ rubocop = ::RuboCop::CLI.new
34
+
35
+ filename = document.file ? "#{document.file}.rb" : "ruby_script.rb"
36
+
37
+ with_ruby_from_stdin(ruby) do
38
+ extract_lints_from_offenses(lint_file(rubocop, filename), source_map)
39
+ end
40
+ end
41
+
42
+ # Defined so we can stub the results in tests
43
+ #
44
+ # @param rubocop [RuboCop::CLI]
45
+ # @param file [String]
46
+ # @return [Array<RuboCop::Cop::Offense>]
47
+ def lint_file(rubocop, file)
48
+ rubocop.run(rubocop_flags << file)
49
+ OffenseCollector.offenses
50
+ end
51
+
52
+ # Aggregates RuboCop offenses and converts them to {SlimLint::Lint}s
53
+ # suitable for reporting.
54
+ #
55
+ # @param offenses [Array<RuboCop::Cop::Offense>]
56
+ # @param source_map [Hash]
57
+ def extract_lints_from_offenses(offenses, source_map)
58
+ offenses.reject! { |offense| config["ignored_cops"].include?(offense.cop_name) }
59
+ offenses.each do |offense|
60
+ @lints << Lint.new(
61
+ [self, offense.cop_name],
62
+ document.file,
63
+ location_for_line(source_map, offense),
64
+ offense.message.gsub(/ at \d+, \d+/, "")
65
+ )
66
+ end
67
+ end
68
+
69
+ # Returns flags that will be passed to RuboCop CLI.
70
+ #
71
+ # @return [Array<String>]
72
+ def rubocop_flags
73
+ flags = %w[--format SlimLint::Linter::RuboCop::OffenseCollector]
74
+ flags += ["--no-display-cop-names"]
75
+ flags += ["--config", ENV["SLIM_LINT_RUBOCOP_CONF"]] if ENV["SLIM_LINT_RUBOCOP_CONF"]
76
+ flags += ["--stdin"]
77
+ flags
78
+ end
79
+
80
+ # Overrides the global stdin to allow RuboCop to read Ruby code from it.
81
+ #
82
+ # @param ruby [String] the Ruby code to write to the overridden stdin
83
+ # @param _block [Block] the block to perform with the overridden stdin
84
+ # @return [void]
85
+ def with_ruby_from_stdin(ruby, &_block)
86
+ original_stdin = $stdin
87
+ stdin = StringIO.new
88
+ stdin.puts(ruby.chomp)
89
+ stdin.rewind
90
+ $stdin = stdin
91
+ yield
92
+ ensure
93
+ $stdin = original_stdin
94
+ end
95
+
96
+ def location_for_line(source_map, offense)
97
+ if source_map.key?(offense.line)
98
+ start = source_map[offense.line].adjust(column: offense.column)
99
+ finish = source_map[offense.last_line].adjust(column: offense.last_column)
100
+ SourceLocation.merge(start, finish, length: offense.column_length)
101
+ else
102
+ SourceLocation.new(start_line: document.source_lines.size, start_column: 0)
103
+ end
104
+ end
105
+
106
+ # Collects offenses detected by RuboCop.
107
+ class OffenseCollector < ::RuboCop::Formatter::BaseFormatter
108
+ class << self
109
+ # List of offenses reported by RuboCop.
110
+ attr_accessor :offenses
111
+ end
112
+
113
+ # Executed when RuboCop begins linting.
114
+ #
115
+ # @param _target_files [Array<String>]
116
+ def started(_target_files)
117
+ self.class.offenses = []
118
+ end
119
+
120
+ # Executed when a file has been scanned by RuboCop, adding the reported
121
+ # offenses to our collection.
122
+ #
123
+ # @param _file [String]
124
+ # @param offenses [Array<RuboCop::Cop::Offense>]
125
+ def file_finished(_file, offenses)
126
+ self.class.offenses += offenses
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end