slim_lint 0.2.0 → 0.3.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/bin/slim-lint +0 -1
  3. data/lib/slim_lint/atom.rb +91 -0
  4. data/lib/slim_lint/capture_map.rb +17 -0
  5. data/lib/slim_lint/cli.rb +25 -8
  6. data/lib/slim_lint/configuration.rb +38 -31
  7. data/lib/slim_lint/configuration_loader.rb +21 -5
  8. data/lib/slim_lint/document.rb +20 -4
  9. data/lib/slim_lint/engine.rb +6 -0
  10. data/lib/slim_lint/file_finder.rb +13 -4
  11. data/lib/slim_lint/filters/inject_line_numbers.rb +7 -2
  12. data/lib/slim_lint/filters/sexp_converter.rb +4 -0
  13. data/lib/slim_lint/lint.rb +17 -1
  14. data/lib/slim_lint/linter/comment_control_statement.rb +2 -2
  15. data/lib/slim_lint/linter/consecutive_control_statements.rb +1 -16
  16. data/lib/slim_lint/linter/empty_control_statement.rb +1 -1
  17. data/lib/slim_lint/linter/redundant_div.rb +8 -5
  18. data/lib/slim_lint/linter/rubocop.rb +40 -18
  19. data/lib/slim_lint/linter/tag_case.rb +1 -1
  20. data/lib/slim_lint/linter.rb +19 -6
  21. data/lib/slim_lint/linter_registry.rb +13 -2
  22. data/lib/slim_lint/linter_selector.rb +74 -0
  23. data/lib/slim_lint/logger.rb +5 -9
  24. data/lib/slim_lint/matcher/anything.rb +9 -0
  25. data/lib/slim_lint/matcher/base.rb +19 -0
  26. data/lib/slim_lint/matcher/capture.rb +30 -0
  27. data/lib/slim_lint/matcher/nothing.rb +11 -0
  28. data/lib/slim_lint/options.rb +16 -6
  29. data/lib/slim_lint/rake_task.rb +15 -0
  30. data/lib/slim_lint/report.rb +7 -0
  31. data/lib/slim_lint/reporter/default_reporter.rb +2 -2
  32. data/lib/slim_lint/reporter/json_reporter.rb +15 -10
  33. data/lib/slim_lint/reporter.rb +17 -11
  34. data/lib/slim_lint/ruby_extractor.rb +2 -0
  35. data/lib/slim_lint/runner.rb +43 -39
  36. data/lib/slim_lint/sexp.rb +43 -30
  37. data/lib/slim_lint/sexp_visitor.rb +66 -28
  38. data/lib/slim_lint/utils.rb +17 -0
  39. data/lib/slim_lint/version.rb +1 -1
  40. data/lib/slim_lint.rb +10 -1
  41. metadata +10 -3
@@ -14,12 +14,18 @@ module SlimLint
14
14
  extractor = SlimLint::RubyExtractor.new
15
15
  extracted_ruby = extractor.extract(processed_sexp)
16
16
 
17
- find_lints(extractor, extracted_ruby) unless extracted_ruby.empty?
17
+ find_lints(extracted_ruby, extractor.source_map) unless extracted_ruby.empty?
18
18
  end
19
19
 
20
20
  private
21
21
 
22
- def find_lints(extractor, ruby)
22
+ # Executes RuboCop against the given Ruby code and records the offenses as
23
+ # lints.
24
+ #
25
+ # @param ruby [String] Ruby code
26
+ # @param source_map [Hash] map of Ruby code line numbers to original line
27
+ # numbers in the template
28
+ def find_lints(ruby, source_map)
23
29
  rubocop = ::RuboCop::CLI.new
24
30
 
25
31
  original_filename = document.file || 'ruby_script'
@@ -30,7 +36,7 @@ module SlimLint
30
36
  begin
31
37
  f.write(ruby)
32
38
  f.close
33
- extract_lints_from_offences(lint_file(rubocop, f.path), extractor)
39
+ extract_lints_from_offenses(lint_file(rubocop, f.path), source_map)
34
40
  ensure
35
41
  f.unlink
36
42
  end
@@ -38,36 +44,52 @@ module SlimLint
38
44
  end
39
45
 
40
46
  # Defined so we can stub the results in tests
47
+ #
48
+ # @param rubocop [RuboCop::CLI]
49
+ # @param file [String]
50
+ # @return [Array<RuboCop::Cop::Offense>]
41
51
  def lint_file(rubocop, file)
42
- rubocop.run(%w[--format SlimLint::OffenceCollector] << file)
43
- OffenceCollector.offences
52
+ rubocop.run(%w[--format SlimLint::OffenseCollector] << file)
53
+ OffenseCollector.offenses
44
54
  end
45
55
 
46
- def extract_lints_from_offences(offences, extractor)
47
- offences.select { |offence| !config['ignored_cops'].include?(offence.cop_name) }
48
- .each do |offence|
56
+ # Aggregates RuboCop offenses and converts them to {SlimLint::Lint}s
57
+ # suitable for reporting.
58
+ #
59
+ # @param offenses [Array<RuboCop::Cop::Offense>]
60
+ # @param source_map [Hash]
61
+ def extract_lints_from_offenses(offenses, source_map)
62
+ offenses.select { |offense| !config['ignored_cops'].include?(offense.cop_name) }
63
+ .each do |offense|
49
64
  @lints << Lint.new(self,
50
65
  document.file,
51
- extractor.source_map[offence.line],
52
- "#{offence.cop_name}: #{offence.message}")
66
+ source_map[offense.line],
67
+ "#{offense.cop_name}: #{offense.message}")
53
68
  end
54
69
  end
55
70
  end
56
71
 
57
- # Collects offences detected by RuboCop.
58
- class OffenceCollector < ::RuboCop::Formatter::BaseFormatter
59
- attr_accessor :offences
60
-
72
+ # Collects offenses detected by RuboCop.
73
+ class OffenseCollector < ::RuboCop::Formatter::BaseFormatter
61
74
  class << self
62
- attr_accessor :offences
75
+ # List of offenses reported by RuboCop.
76
+ attr_accessor :offenses
63
77
  end
64
78
 
79
+ # Executed when RuboCop begins linting.
80
+ #
81
+ # @param _target_files [Array<String>]
65
82
  def started(_target_files)
66
- self.class.offences = []
83
+ self.class.offenses = []
67
84
  end
68
85
 
69
- def file_finished(_file, offences)
70
- self.class.offences += offences
86
+ # Executed when a file has been scanned by RuboCop, adding the reported
87
+ # offenses to our collection.
88
+ #
89
+ # @param _file [String]
90
+ # @param offenses [Array<RuboCop::Cop::Offense>]
91
+ def file_finished(_file, offenses)
92
+ self.class.offenses += offenses
71
93
  end
72
94
  end
73
95
  end
@@ -5,7 +5,7 @@ module SlimLint
5
5
 
6
6
  on [:html, :tag] do |sexp|
7
7
  _, _, name = sexp
8
- next unless name =~ /[A-Z]/
8
+ next unless name[/[A-Z]/]
9
9
 
10
10
  report_lint(sexp, "Tag `#{name}` should be written as `#{name.downcase}`")
11
11
  end
@@ -1,22 +1,29 @@
1
1
  module SlimLint
2
2
  # Base implementation for all lint checks.
3
+ #
4
+ # @abstract
3
5
  class Linter
4
6
  # Include definitions for Sexp pattern-matching helpers.
5
7
  include SexpVisitor
6
8
  extend SexpVisitor::DSL
7
9
 
8
- # TODO: Remove once spec support returns an array of lints instead of a
9
- # linter
10
+ # List of lints reported by this linter.
11
+ #
12
+ # @todo Remove once spec/support/shared_linter_context returns an array of
13
+ # lints for the subject instead of the linter itself.
10
14
  attr_reader :lints
11
15
 
16
+ # Initializes a linter with the specified configuration.
17
+ #
12
18
  # @param config [Hash] configuration for this linter
13
19
  def initialize(config)
14
20
  @config = config
15
21
  @lints = []
16
- @ruby_parser = nil
17
22
  end
18
23
 
19
- # Runs the linter against the specified Sexp
24
+ # Runs the linter against the given Slim document.
25
+ #
26
+ # @param document [SlimLint::Document]
20
27
  def run(document)
21
28
  @document = document
22
29
  @lints = []
@@ -25,6 +32,8 @@ module SlimLint
25
32
  end
26
33
 
27
34
  # Returns the simple name for this linter.
35
+ #
36
+ # @return [String]
28
37
  def name
29
38
  self.class.name.split('::').last
30
39
  end
@@ -34,12 +43,16 @@ module SlimLint
34
43
  attr_reader :config, :document
35
44
 
36
45
  # Record a lint for reporting back to the user.
37
- def report_lint(sexp, message)
38
- @lints << SlimLint::Lint.new(self, @document.file, sexp.line, message)
46
+ #
47
+ # @param node [#line] node to extract the line number from
48
+ # @param message [String] error/warning to display to the user
49
+ def report_lint(node, message)
50
+ @lints << SlimLint::Lint.new(self, @document.file, node.line, message)
39
51
  end
40
52
 
41
53
  # Parse Ruby code into an abstract syntax tree.
42
54
  #
55
+ # @param source [String] Ruby code to parse
43
56
  # @return [AST::Node]
44
57
  def parse_ruby(source)
45
58
  @ruby_parser ||= SlimLint::RubyParser.new
@@ -6,12 +6,23 @@ module SlimLint
6
6
  @linters = []
7
7
 
8
8
  class << self
9
+ # List of all registered linters.
9
10
  attr_reader :linters
10
11
 
11
- def included(base)
12
- @linters << base
12
+ # Executed when a linter includes the {LinterRegistry} module.
13
+ #
14
+ # This results in the linter being registered with the registry.
15
+ #
16
+ # @param subclass [Class]
17
+ def included(subclass)
18
+ @linters << subclass
13
19
  end
14
20
 
21
+ # Return a list of {SlimLint::Linter} {Class}es corresponding to the
22
+ # specified list of names.
23
+ #
24
+ # @param linter_names [Array<String>]
25
+ # @return [Array<Class>]
15
26
  def extract_linters_from(linter_names)
16
27
  linter_names.map do |linter_name|
17
28
  begin
@@ -0,0 +1,74 @@
1
+ module SlimLint
2
+ # Chooses the appropriate linters to run given the specified configuration.
3
+ class LinterSelector
4
+ # Creates a selector using the given configuration and additional options.
5
+ def initialize(config, options)
6
+ @config = config
7
+ @options = options
8
+ end
9
+
10
+ # Returns the set of linters to run against the given file.
11
+ #
12
+ # @param file [String]
13
+ # @raise [SlimLint::Exceptions::NoLintersError] when no linters are enabled
14
+ # @return [Array<SlimLint::Linter>]
15
+ def linters_for_file(file)
16
+ @linters ||= extract_enabled_linters(@config, @options)
17
+ @linters.select { |linter| run_linter_on_file?(@config, linter, file) }
18
+ end
19
+
20
+ private
21
+
22
+ # Returns a list of linters that are enabled given the specified
23
+ # configuration and additional options.
24
+ #
25
+ # @param config [SlimLint::Configuration]
26
+ # @param options [Hash]
27
+ # @return [Array<SlimLint::Linter>]
28
+ def extract_enabled_linters(config, options)
29
+ included_linters = LinterRegistry
30
+ .extract_linters_from(options.fetch(:included_linters, []))
31
+
32
+ included_linters = LinterRegistry.linters if included_linters.empty?
33
+
34
+ excluded_linters = LinterRegistry
35
+ .extract_linters_from(options.fetch(:excluded_linters, []))
36
+
37
+ # After filtering out explicitly included/excluded linters, only include
38
+ # linters which are enabled in the configuration
39
+ linters = (included_linters - excluded_linters).map do |linter_class|
40
+ linter_config = config.for_linter(linter_class)
41
+ linter_class.new(linter_config) if linter_config['enabled']
42
+ end.compact
43
+
44
+ # Highlight condition where all linters were filtered out, as this was
45
+ # likely a mistake on the user's part
46
+ if linters.empty?
47
+ raise SlimLint::Exceptions::NoLintersError, 'No linters specified'
48
+ end
49
+
50
+ linters
51
+ end
52
+
53
+ # Whether to run the given linter against the specified file.
54
+ #
55
+ # @param config [SlimLint::Configuration]
56
+ # @param linter [SlimLint::Linter]
57
+ # @param file [String]
58
+ # @return [Boolean]
59
+ def run_linter_on_file?(config, linter, file)
60
+ linter_config = config.for_linter(linter)
61
+
62
+ if linter_config['include'].any? &&
63
+ !SlimLint::Utils.any_glob_matches?(linter_config['include'], file)
64
+ return false
65
+ end
66
+
67
+ if SlimLint::Utils.any_glob_matches?(linter_config['exclude'], file)
68
+ return false
69
+ end
70
+
71
+ true
72
+ end
73
+ end
74
+ end
@@ -73,15 +73,6 @@ module SlimLint
73
73
  color(33, *args)
74
74
  end
75
75
 
76
- # Print specified output in bold face in a color indicative of a warning.
77
- # If output destination is not a TTY, behaves the same as {#log}.
78
- #
79
- # @param args [Array<String>]
80
- # @return [nil]
81
- def bold_warning(*args)
82
- color('1;33', *args)
83
- end
84
-
85
76
  # Print the specified output in a color indicating information.
86
77
  # If output destination is not a TTY, behaves the same as {#log}.
87
78
  #
@@ -100,6 +91,11 @@ module SlimLint
100
91
 
101
92
  private
102
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
103
99
  def color(code, output, newline = true)
104
100
  log(color_enabled ? "\033[#{code}m#{output}\033[0m" : output, newline)
105
101
  end
@@ -0,0 +1,9 @@
1
+ module SlimLint::Matcher
2
+ # Will match anything, acting as a wildcard.
3
+ class Anything < Base
4
+ # @see {SlimLint::Matcher::Base#match?}
5
+ def match?(*)
6
+ true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ module SlimLint::Matcher
2
+ # Represents a Sexp pattern implementing complex matching logic.
3
+ #
4
+ # Subclasses can implement custom logic to create complex matches that can be
5
+ # reused across linters, DRYing up matching code.
6
+ #
7
+ # @abstract
8
+ class Base
9
+ # Whether this matcher matches the specified object.
10
+ #
11
+ # This must be implemented by subclasses.
12
+ #
13
+ # @param other [Object]
14
+ # @return [Boolean]
15
+ def match?(*)
16
+ raise NotImplementedError, 'Matcher must implement `match?`'
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ module SlimLint::Matcher
2
+ # Wraps a matcher, taking on the behavior of the wrapped matcher but storing
3
+ # the value that matched so it can be referred to later.
4
+ class Capture < Base
5
+ # @return [SlimLint::Matcher::Base] matcher that this capture wraps
6
+ attr_accessor :matcher
7
+
8
+ # @return [Object] value that was captured
9
+ attr_accessor :value
10
+
11
+ # Creates a capture that wraps that given matcher.
12
+ #
13
+ # @param matcher [SlimLint::Matcher::Base]
14
+ # @return [SlimLint::Matcher::Capture]
15
+ def self.from_matcher(matcher)
16
+ new.tap do |cap_matcher|
17
+ cap_matcher.matcher = matcher
18
+ end
19
+ end
20
+
21
+ # @see {SlimLint::Matcher::Base#match?}
22
+ def match?(object)
23
+ if result = @matcher.match?(object)
24
+ @value = object
25
+ end
26
+
27
+ result
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ module SlimLint::Matcher
2
+ # Does not match anything.
3
+ #
4
+ # This is used in specs.
5
+ class Nothing < Base
6
+ # @see {SlimLint::Matcher::Base#match?}
7
+ def match?(*)
8
+ false
9
+ end
10
+ end
11
+ end
@@ -30,12 +30,8 @@ module SlimLint
30
30
 
31
31
  private
32
32
 
33
+ # Register linter-related flags.
33
34
  def add_linter_options(parser)
34
- parser.on('-e', '--exclude file,...', Array,
35
- 'List of file names to exclude') do |files|
36
- @options[:excluded_files] = files
37
- end
38
-
39
35
  parser.on('-i', '--include-linter linter,...', Array,
40
36
  'Specify which linters you want to run') do |linters|
41
37
  @options[:included_linters] = linters
@@ -48,10 +44,23 @@ module SlimLint
48
44
 
49
45
  parser.on('-r', '--reporter reporter', String,
50
46
  'Specify which reporter you want to use to generate the output') do |reporter|
51
- @options[:reporter] = SlimLint::Reporter.const_get("#{reporter.capitalize}Reporter")
47
+ @options[:reporter] = load_reporter_class(reporter.capitalize)
52
48
  end
53
49
  end
54
50
 
51
+ # Returns the class of the specified Reporter.
52
+ #
53
+ # @param reporter_name [String]
54
+ # @raise [SlimLint::Exceptions::InvalidCLIOption] if reporter doesn't exist
55
+ # @return [Class]
56
+ def load_reporter_class(reporter_name)
57
+ SlimLint::Reporter.const_get("#{reporter_name}Reporter")
58
+ rescue NameError
59
+ raise SlimLint::Exceptions::InvalidCLIOption,
60
+ "#{reporter_name}Reporter does not exist"
61
+ end
62
+
63
+ # Register file-related flags.
55
64
  def add_file_options(parser)
56
65
  parser.on('-c', '--config config-file', String,
57
66
  'Specify which configuration file you want to use') do |conf_file|
@@ -64,6 +73,7 @@ module SlimLint
64
73
  end
65
74
  end
66
75
 
76
+ # Register informational flags.
67
77
  def add_info_options(parser)
68
78
  parser.on('--show-linters', 'Display available linters') do
69
79
  @options[:show_linters] = true
@@ -51,6 +51,8 @@ module SlimLint
51
51
  attr_accessor :quiet
52
52
 
53
53
  # Create the task so it exists in the current namespace.
54
+ #
55
+ # @param name [Symbol] task name
54
56
  def initialize(name = :slim_lint)
55
57
  @name = name
56
58
  @files = ['.'] # Search for everything under current directory by default
@@ -63,6 +65,7 @@ module SlimLint
63
65
 
64
66
  private
65
67
 
68
+ # Defines the Rake task.
66
69
  def define
67
70
  desc default_description unless ::Rake.application.last_description
68
71
 
@@ -75,6 +78,9 @@ module SlimLint
75
78
  end
76
79
  end
77
80
 
81
+ # Executes the CLI given the specified task arguments.
82
+ #
83
+ # @param task_args [Rake::TaskArguments]
78
84
  def run_cli(task_args)
79
85
  cli_args = ['--config', config] if config
80
86
 
@@ -84,6 +90,10 @@ module SlimLint
84
90
  fail "#{SlimLint::APP_NAME} failed with exit code #{result}" unless result == 0
85
91
  end
86
92
 
93
+ # Returns the list of files that should be linted given the specified task
94
+ # arguments.
95
+ #
96
+ # @param task_args [Rake::TaskArguments]
87
97
  def files_to_lint(task_args)
88
98
  # Note: we're abusing Rake's argument handling a bit here. We call the
89
99
  # first argument `files` but it's actually only the first file--we pull
@@ -96,6 +106,11 @@ module SlimLint
96
106
  end
97
107
 
98
108
  # Friendly description that shows the full command that will be executed.
109
+ #
110
+ # This allows us to change the information displayed by `rake --tasks` based
111
+ # on the options passed to the constructor which defined the task.
112
+ #
113
+ # @return [String]
99
114
  def default_description
100
115
  description = "Run `#{SlimLint::APP_NAME}"
101
116
  description += " --config #{config}" if config
@@ -1,9 +1,16 @@
1
1
  module SlimLint
2
2
  # Contains information about all lints detected during a scan.
3
3
  class Report
4
+ # List of lints that were found.
4
5
  attr_accessor :lints
6
+
7
+ # List of files that were linted.
5
8
  attr_reader :files
6
9
 
10
+ # Creates a report.
11
+ #
12
+ # @param lints [Array<SlimLint::Lint>] lints that were found
13
+ # @param files [Array<String>] files that were linted
7
14
  def initialize(lints, files)
8
15
  @lints = lints.sort_by { |l| [l.filename, l.line] }
9
16
  @files = files
@@ -2,8 +2,8 @@ module SlimLint
2
2
  # Outputs lints in a simple format with the filename, line number, and lint
3
3
  # message.
4
4
  class Reporter::DefaultReporter < Reporter
5
- def report_lints
6
- sorted_lints = lints.sort_by { |l| [l.filename, l.line] }
5
+ def display_report(report)
6
+ sorted_lints = report.lints.sort_by { |l| [l.filename, l.line] }
7
7
 
8
8
  sorted_lints.each do |lint|
9
9
  print_location(lint)
@@ -1,29 +1,34 @@
1
1
  module SlimLint
2
2
  # Outputs report as a JSON document.
3
3
  class Reporter::JsonReporter < Reporter
4
- def report_lints
4
+ def display_report(report)
5
+ lints = report.lints
5
6
  grouped = lints.group_by(&:filename)
6
7
 
7
- report = {
8
- metadata: {
9
- slim_lint_version: SlimLint::VERSION,
10
- ruby_engine: RUBY_ENGINE,
11
- ruby_patchlevel: RUBY_PATCHLEVEL.to_s,
12
- ruby_platform: RUBY_PLATFORM,
13
- },
8
+ report_hash = {
9
+ metadata: metadata,
14
10
  files: grouped.map { |l| map_file(l) },
15
11
  summary: {
16
12
  offense_count: lints.length,
17
13
  target_file_count: grouped.length,
18
- inspected_file_count: files.length,
14
+ inspected_file_count: report.files.length,
19
15
  },
20
16
  }
21
17
 
22
- log.log report.to_json
18
+ log.log report_hash.to_json
23
19
  end
24
20
 
25
21
  private
26
22
 
23
+ def metadata
24
+ {
25
+ slim_lint_version: SlimLint::VERSION,
26
+ ruby_engine: RUBY_ENGINE,
27
+ ruby_patchlevel: RUBY_PATCHLEVEL.to_s,
28
+ ruby_platform: RUBY_PLATFORM,
29
+ }
30
+ end
31
+
27
32
  def map_file(file)
28
33
  {
29
34
  path: file.first,
@@ -1,36 +1,42 @@
1
1
  module SlimLint
2
- # Abstract lint reporter. Subclass and override {#report_lints} to
2
+ # Abstract lint reporter. Subclass and override {#display_report} to
3
3
  # implement a custom lint reporter.
4
4
  #
5
5
  # @abstract
6
6
  class Reporter
7
- attr_reader :lints
8
- attr_reader :files
9
-
7
+ # Creates the reporter that will display the given report.
8
+ #
10
9
  # @param logger [SlimLint::Logger]
11
- # @param report [SlimLint::Report]
12
- def initialize(logger, report)
10
+ def initialize(logger)
13
11
  @log = logger
14
- @lints = report.lints
15
- @files = report.files
16
12
  end
17
13
 
18
14
  # Implemented by subclasses to display lints from a {SlimLint::Report}.
19
- def report_lints
20
- raise NotImplementedError
15
+ #
16
+ # @param report [SlimLint::Report]
17
+ def display_report(report)
18
+ raise NotImplementedError,
19
+ "Implement `display_report` to display #{report}"
21
20
  end
22
21
 
23
- # Keep tracking all the descendants of this class for the list of available reporters
22
+ # Keep tracking all the descendants of this class for the list of available
23
+ # reporters.
24
+ #
25
+ # @return [Array<Class>]
24
26
  def self.descendants
25
27
  @descendants ||= []
26
28
  end
27
29
 
30
+ # Executed when this class is subclassed.
31
+ #
32
+ # @param descendant [Class]
28
33
  def self.inherited(descendant)
29
34
  descendants << descendant
30
35
  end
31
36
 
32
37
  private
33
38
 
39
+ # @return [SlimLint::Logger] logger to send output to
34
40
  attr_reader :log
35
41
  end
36
42
  end
@@ -30,6 +30,8 @@ module SlimLint
30
30
  include SexpVisitor
31
31
  extend SexpVisitor::DSL
32
32
 
33
+ # Map of generated Ruby source code lines and their corresponding lines in
34
+ # the original document.
33
35
  attr_reader :source_map
34
36
 
35
37
  # Extracts Ruby code from Sexp representing a Slim document.