scss_lint 0.38.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/bin/scss-lint +6 -0
  3. data/config/default.yml +205 -0
  4. data/data/prefixed-identifiers/base.txt +107 -0
  5. data/data/prefixed-identifiers/bourbon.txt +71 -0
  6. data/data/properties.txt +477 -0
  7. data/data/property-sort-orders/concentric.txt +134 -0
  8. data/data/property-sort-orders/recess.txt +149 -0
  9. data/data/property-sort-orders/smacss.txt +137 -0
  10. data/lib/scss_lint.rb +31 -0
  11. data/lib/scss_lint/cli.rb +215 -0
  12. data/lib/scss_lint/config.rb +251 -0
  13. data/lib/scss_lint/constants.rb +8 -0
  14. data/lib/scss_lint/control_comment_processor.rb +126 -0
  15. data/lib/scss_lint/engine.rb +56 -0
  16. data/lib/scss_lint/exceptions.rb +21 -0
  17. data/lib/scss_lint/file_finder.rb +68 -0
  18. data/lib/scss_lint/lint.rb +24 -0
  19. data/lib/scss_lint/linter.rb +161 -0
  20. data/lib/scss_lint/linter/bang_format.rb +52 -0
  21. data/lib/scss_lint/linter/border_zero.rb +39 -0
  22. data/lib/scss_lint/linter/color_keyword.rb +32 -0
  23. data/lib/scss_lint/linter/color_variable.rb +60 -0
  24. data/lib/scss_lint/linter/comment.rb +21 -0
  25. data/lib/scss_lint/linter/compass.rb +7 -0
  26. data/lib/scss_lint/linter/compass/property_with_mixin.rb +47 -0
  27. data/lib/scss_lint/linter/debug_statement.rb +10 -0
  28. data/lib/scss_lint/linter/declaration_order.rb +71 -0
  29. data/lib/scss_lint/linter/duplicate_property.rb +58 -0
  30. data/lib/scss_lint/linter/else_placement.rb +48 -0
  31. data/lib/scss_lint/linter/empty_line_between_blocks.rb +85 -0
  32. data/lib/scss_lint/linter/empty_rule.rb +11 -0
  33. data/lib/scss_lint/linter/final_newline.rb +20 -0
  34. data/lib/scss_lint/linter/hex_length.rb +56 -0
  35. data/lib/scss_lint/linter/hex_notation.rb +38 -0
  36. data/lib/scss_lint/linter/hex_validation.rb +23 -0
  37. data/lib/scss_lint/linter/id_selector.rb +10 -0
  38. data/lib/scss_lint/linter/import_path.rb +62 -0
  39. data/lib/scss_lint/linter/important_rule.rb +12 -0
  40. data/lib/scss_lint/linter/indentation.rb +197 -0
  41. data/lib/scss_lint/linter/leading_zero.rb +49 -0
  42. data/lib/scss_lint/linter/mergeable_selector.rb +60 -0
  43. data/lib/scss_lint/linter/name_format.rb +117 -0
  44. data/lib/scss_lint/linter/nesting_depth.rb +24 -0
  45. data/lib/scss_lint/linter/placeholder_in_extend.rb +22 -0
  46. data/lib/scss_lint/linter/property_count.rb +44 -0
  47. data/lib/scss_lint/linter/property_sort_order.rb +198 -0
  48. data/lib/scss_lint/linter/property_spelling.rb +49 -0
  49. data/lib/scss_lint/linter/property_units.rb +59 -0
  50. data/lib/scss_lint/linter/qualifying_element.rb +42 -0
  51. data/lib/scss_lint/linter/selector_depth.rb +64 -0
  52. data/lib/scss_lint/linter/selector_format.rb +102 -0
  53. data/lib/scss_lint/linter/shorthand.rb +139 -0
  54. data/lib/scss_lint/linter/single_line_per_property.rb +59 -0
  55. data/lib/scss_lint/linter/single_line_per_selector.rb +35 -0
  56. data/lib/scss_lint/linter/space_after_comma.rb +110 -0
  57. data/lib/scss_lint/linter/space_after_property_colon.rb +92 -0
  58. data/lib/scss_lint/linter/space_after_property_name.rb +27 -0
  59. data/lib/scss_lint/linter/space_before_brace.rb +72 -0
  60. data/lib/scss_lint/linter/space_between_parens.rb +35 -0
  61. data/lib/scss_lint/linter/string_quotes.rb +94 -0
  62. data/lib/scss_lint/linter/trailing_semicolon.rb +67 -0
  63. data/lib/scss_lint/linter/trailing_zero.rb +41 -0
  64. data/lib/scss_lint/linter/unnecessary_mantissa.rb +42 -0
  65. data/lib/scss_lint/linter/unnecessary_parent_reference.rb +49 -0
  66. data/lib/scss_lint/linter/url_format.rb +56 -0
  67. data/lib/scss_lint/linter/url_quotes.rb +27 -0
  68. data/lib/scss_lint/linter/variable_for_property.rb +30 -0
  69. data/lib/scss_lint/linter/vendor_prefix.rb +64 -0
  70. data/lib/scss_lint/linter/zero_unit.rb +39 -0
  71. data/lib/scss_lint/linter_registry.rb +26 -0
  72. data/lib/scss_lint/location.rb +38 -0
  73. data/lib/scss_lint/options.rb +109 -0
  74. data/lib/scss_lint/rake_task.rb +106 -0
  75. data/lib/scss_lint/reporter.rb +18 -0
  76. data/lib/scss_lint/reporter/config_reporter.rb +26 -0
  77. data/lib/scss_lint/reporter/default_reporter.rb +27 -0
  78. data/lib/scss_lint/reporter/files_reporter.rb +8 -0
  79. data/lib/scss_lint/reporter/json_reporter.rb +30 -0
  80. data/lib/scss_lint/reporter/xml_reporter.rb +33 -0
  81. data/lib/scss_lint/runner.rb +51 -0
  82. data/lib/scss_lint/sass/script.rb +78 -0
  83. data/lib/scss_lint/sass/tree.rb +168 -0
  84. data/lib/scss_lint/selector_visitor.rb +34 -0
  85. data/lib/scss_lint/utils.rb +112 -0
  86. data/lib/scss_lint/version.rb +4 -0
  87. data/spec/scss_lint/cli_spec.rb +177 -0
  88. data/spec/scss_lint/config_spec.rb +253 -0
  89. data/spec/scss_lint/engine_spec.rb +24 -0
  90. data/spec/scss_lint/file_finder_spec.rb +134 -0
  91. data/spec/scss_lint/linter/bang_format_spec.rb +121 -0
  92. data/spec/scss_lint/linter/border_zero_spec.rb +118 -0
  93. data/spec/scss_lint/linter/color_keyword_spec.rb +83 -0
  94. data/spec/scss_lint/linter/color_variable_spec.rb +155 -0
  95. data/spec/scss_lint/linter/comment_spec.rb +79 -0
  96. data/spec/scss_lint/linter/compass/property_with_mixin_spec.rb +55 -0
  97. data/spec/scss_lint/linter/debug_statement_spec.rb +21 -0
  98. data/spec/scss_lint/linter/declaration_order_spec.rb +575 -0
  99. data/spec/scss_lint/linter/duplicate_property_spec.rb +189 -0
  100. data/spec/scss_lint/linter/else_placement_spec.rb +106 -0
  101. data/spec/scss_lint/linter/empty_line_between_blocks_spec.rb +276 -0
  102. data/spec/scss_lint/linter/empty_rule_spec.rb +27 -0
  103. data/spec/scss_lint/linter/final_newline_spec.rb +49 -0
  104. data/spec/scss_lint/linter/hex_length_spec.rb +104 -0
  105. data/spec/scss_lint/linter/hex_notation_spec.rb +104 -0
  106. data/spec/scss_lint/linter/hex_validation_spec.rb +40 -0
  107. data/spec/scss_lint/linter/id_selector_spec.rb +62 -0
  108. data/spec/scss_lint/linter/import_path_spec.rb +300 -0
  109. data/spec/scss_lint/linter/important_rule_spec.rb +43 -0
  110. data/spec/scss_lint/linter/indentation_spec.rb +347 -0
  111. data/spec/scss_lint/linter/leading_zero_spec.rb +233 -0
  112. data/spec/scss_lint/linter/mergeable_selector_spec.rb +283 -0
  113. data/spec/scss_lint/linter/name_format_spec.rb +282 -0
  114. data/spec/scss_lint/linter/nesting_depth_spec.rb +114 -0
  115. data/spec/scss_lint/linter/placeholder_in_extend_spec.rb +63 -0
  116. data/spec/scss_lint/linter/property_count_spec.rb +104 -0
  117. data/spec/scss_lint/linter/property_sort_order_spec.rb +482 -0
  118. data/spec/scss_lint/linter/property_spelling_spec.rb +84 -0
  119. data/spec/scss_lint/linter/property_units_spec.rb +229 -0
  120. data/spec/scss_lint/linter/qualifying_element_spec.rb +125 -0
  121. data/spec/scss_lint/linter/selector_depth_spec.rb +159 -0
  122. data/spec/scss_lint/linter/selector_format_spec.rb +632 -0
  123. data/spec/scss_lint/linter/shorthand_spec.rb +198 -0
  124. data/spec/scss_lint/linter/single_line_per_property_spec.rb +73 -0
  125. data/spec/scss_lint/linter/single_line_per_selector_spec.rb +130 -0
  126. data/spec/scss_lint/linter/space_after_comma_spec.rb +332 -0
  127. data/spec/scss_lint/linter/space_after_property_colon_spec.rb +373 -0
  128. data/spec/scss_lint/linter/space_after_property_name_spec.rb +37 -0
  129. data/spec/scss_lint/linter/space_before_brace_spec.rb +829 -0
  130. data/spec/scss_lint/linter/space_between_parens_spec.rb +263 -0
  131. data/spec/scss_lint/linter/string_quotes_spec.rb +335 -0
  132. data/spec/scss_lint/linter/trailing_semicolon_spec.rb +304 -0
  133. data/spec/scss_lint/linter/trailing_zero_spec.rb +176 -0
  134. data/spec/scss_lint/linter/unnecessary_mantissa_spec.rb +67 -0
  135. data/spec/scss_lint/linter/unnecessary_parent_reference_spec.rb +98 -0
  136. data/spec/scss_lint/linter/url_format_spec.rb +55 -0
  137. data/spec/scss_lint/linter/url_quotes_spec.rb +73 -0
  138. data/spec/scss_lint/linter/variable_for_property_spec.rb +145 -0
  139. data/spec/scss_lint/linter/vendor_prefix_spec.rb +371 -0
  140. data/spec/scss_lint/linter/zero_unit_spec.rb +113 -0
  141. data/spec/scss_lint/linter_registry_spec.rb +50 -0
  142. data/spec/scss_lint/linter_spec.rb +292 -0
  143. data/spec/scss_lint/location_spec.rb +42 -0
  144. data/spec/scss_lint/options_spec.rb +34 -0
  145. data/spec/scss_lint/rake_task_spec.rb +43 -0
  146. data/spec/scss_lint/reporter/config_reporter_spec.rb +42 -0
  147. data/spec/scss_lint/reporter/default_reporter_spec.rb +73 -0
  148. data/spec/scss_lint/reporter/files_reporter_spec.rb +38 -0
  149. data/spec/scss_lint/reporter/json_reporter_spec.rb +96 -0
  150. data/spec/scss_lint/reporter/xml_reporter_spec.rb +103 -0
  151. data/spec/scss_lint/reporter_spec.rb +11 -0
  152. data/spec/scss_lint/runner_spec.rb +123 -0
  153. data/spec/scss_lint/selector_visitor_spec.rb +264 -0
  154. data/spec/spec_helper.rb +34 -0
  155. data/spec/support/isolated_environment.rb +25 -0
  156. data/spec/support/matchers/report_lint.rb +48 -0
  157. metadata +328 -0
@@ -0,0 +1,8 @@
1
+ # Global application constants.
2
+ module SCSSLint
3
+ SCSS_LINT_HOME = File.realpath(File.join(File.dirname(__FILE__), '..', '..'))
4
+ SCSS_LINT_DATA = File.join(SCSS_LINT_HOME, 'data')
5
+
6
+ REPO_URL = 'https://github.com/brigade/scss-lint'
7
+ BUG_REPORT_URL = "#{REPO_URL}/issues"
8
+ end
@@ -0,0 +1,126 @@
1
+ require 'set'
2
+
3
+ module SCSSLint
4
+ # Tracks which lines have been disabled for a given linter.
5
+ class ControlCommentProcessor
6
+ def initialize(linter)
7
+ @disable_stack = []
8
+ @disabled_lines = Set.new
9
+ @linter = linter
10
+ end
11
+
12
+ # Filter lints given the comments that were processed in the document.
13
+ #
14
+ # @param lints [Array<SCSSLint::Lint>]
15
+ def filter_lints(lints)
16
+ lints.reject { |lint| @disabled_lines.include?(lint.location.line) }
17
+ end
18
+
19
+ # Executed before a node has been visited.
20
+ #
21
+ # @param node [Sass::Tree::Node]
22
+ def before_node_visit(node)
23
+ return unless command = extract_command(node)
24
+
25
+ linters = command[:linters]
26
+ return unless linters.include?('all') || linters.include?(@linter.name)
27
+
28
+ process_command(command[:action], node)
29
+
30
+ # Is the control comment the only thing on this line?
31
+ return if node.is_a?(Sass::Tree::RuleNode) ||
32
+ %r{^\s*(//|/\*)}.match(@linter.engine.lines[node.line - 1])
33
+
34
+ # Otherwise, pop since we only want comment to apply to the single line
35
+ pop_control_comment_stack(node)
36
+ end
37
+
38
+ # Executed after a node has been visited.
39
+ #
40
+ # @param node [Sass::Tree::Node]
41
+ def after_node_visit(node)
42
+ while @disable_stack.any? && @disable_stack.last.node_parent == node
43
+ pop_control_comment_stack(node)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def extract_command(node)
50
+ comment =
51
+ case node
52
+ when Sass::Tree::CommentNode
53
+ node.value.first
54
+ when Sass::Tree::RuleNode
55
+ node.rule.select { |chunk| chunk.is_a?(String) }.join
56
+ end
57
+
58
+ return unless match = %r{
59
+ (/|\*)\s* # Comment start marker
60
+ scss-lint:
61
+ (?<action>disable|enable)\s+
62
+ (?<linters>.*?)
63
+ \s*(?:\*/|\n) # Comment end marker or end of line
64
+ }x.match(comment)
65
+
66
+ {
67
+ action: match[:action],
68
+ linters: match[:linters].split(/\s*,\s*|\s+/),
69
+ }
70
+ end
71
+
72
+ def process_command(command, node)
73
+ case command
74
+ when 'disable'
75
+ @disable_stack << node
76
+ when 'enable'
77
+ pop_control_comment_stack(node)
78
+ end
79
+ end
80
+
81
+ def pop_control_comment_stack(node)
82
+ return unless comment_node = @disable_stack.pop
83
+
84
+ start_line = comment_node.line
85
+
86
+ # Find the deepest child that has a line number to which a lint might
87
+ # apply (if it is a control comment enable node, it will be the line of
88
+ # the comment itself).
89
+ child = node
90
+ prev_child = node
91
+ until [nil, prev_child].include?(child = last_child(child))
92
+ prev_child = child
93
+ end
94
+
95
+ # Fall back to prev_child if last_child() returned nil (i.e. node had no
96
+ # children with line numbers)
97
+ end_line = (child || prev_child).line
98
+
99
+ @disabled_lines.merge(start_line..end_line)
100
+ end
101
+
102
+ # Gets the child of the node that resides on the lowest line in the file.
103
+ #
104
+ # This is necessary due to the fact that our monkey patching of the parse
105
+ # tree's {#children} method does not return nodes sorted by their line
106
+ # number.
107
+ #
108
+ # Returns `nil` if node has no children or no children with associated line
109
+ # numbers.
110
+ #
111
+ # @param node [Sass::Tree::Node, Sass::Script::Tree::Node]
112
+ # @return [Sass::Tree::Node, Sass::Script::Tree::Node]
113
+ def last_child(node)
114
+ last = node.children.inject(node) do |lowest, child|
115
+ return lowest unless child.respond_to?(:line)
116
+ lowest.line < child.line ? child : lowest
117
+ end
118
+
119
+ # In this case, none of the children have associated line numbers or the
120
+ # node has no children at all, so return `nil`.
121
+ return if last == node
122
+
123
+ last
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,56 @@
1
+ require 'sass'
2
+
3
+ module SCSSLint
4
+ class FileEncodingError < StandardError; end
5
+
6
+ # Contains all information for a parsed SCSS file, including its name,
7
+ # contents, and parse tree.
8
+ class Engine
9
+ ENGINE_OPTIONS = { cache: false, syntax: :scss }
10
+
11
+ attr_reader :contents, :filename, :lines, :tree
12
+
13
+ # Creates a parsed representation of an SCSS document from the given string
14
+ # or file.
15
+ #
16
+ # @param options [Hash]
17
+ # @option options [String] :file The file to load
18
+ # @option options [String] :code The code to parse
19
+ def initialize(options = {})
20
+ if options[:file]
21
+ build_from_file(options[:file])
22
+ elsif options[:code]
23
+ build_from_string(options[:code])
24
+ end
25
+
26
+ # Need to force encoding to avoid Windows-related bugs.
27
+ # Need `to_a` for Ruby 1.9.3.
28
+ @lines = @contents.force_encoding('UTF-8').lines.to_a
29
+ @tree = @engine.to_tree
30
+ rescue Encoding::UndefinedConversionError, Sass::SyntaxError => error
31
+ if error.is_a?(Encoding::UndefinedConversionError) ||
32
+ error.message.match(/invalid.*(byte sequence|character)/i)
33
+ raise FileEncodingError,
34
+ "Unable to parse SCSS file: #{error}",
35
+ error.backtrace
36
+ else
37
+ raise
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # @param path [String]
44
+ def build_from_file(path)
45
+ @filename = path
46
+ @engine = Sass::Engine.for_file(path, ENGINE_OPTIONS)
47
+ @contents = File.open(path, 'r').read
48
+ end
49
+
50
+ # @param scss [String]
51
+ def build_from_string(scss)
52
+ @engine = Sass::Engine.new(scss, ENGINE_OPTIONS)
53
+ @contents = scss
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,21 @@
1
+ module SCSSLint::Exceptions
2
+ # Raised when all files matched by the specified glob patterns were filtered
3
+ # by exclude patterns.
4
+ class AllFilesFilteredError < StandardError; end
5
+
6
+ # Raised when an invalid flag is given via the command line.
7
+ class InvalidCLIOption < StandardError; end
8
+
9
+ # Raised when the configuration file is invalid for some reason.
10
+ class InvalidConfiguration < StandardError; end
11
+
12
+ # Raised when an unexpected error occurs in a linter
13
+ class LinterError < StandardError; end
14
+
15
+ # Raised when no files were specified or specified glob patterns did not match
16
+ # any files.
17
+ class NoFilesError < StandardError; end
18
+
19
+ # Raised when a required library (specified via command line) does not exist.
20
+ class RequiredLibraryMissingError < StandardError; end
21
+ end
@@ -0,0 +1,68 @@
1
+ require 'find'
2
+
3
+ module SCSSLint
4
+ # Finds all SCSS files that should be linted given a set of paths, globs, and
5
+ # configuration.
6
+ class FileFinder
7
+ # List of extensions of files to include when only a directory is specified
8
+ # as a path.
9
+ VALID_EXTENSIONS = %w[.css .scss]
10
+
11
+ # Create a {FileFinder}.
12
+ #
13
+ # @param config [SCSSLint::Config]
14
+ def initialize(config)
15
+ @config = config
16
+ end
17
+
18
+ # Find all files that match given the specified options.
19
+ #
20
+ # @param patterns [Array<String>] a list of file paths and glob patterns
21
+ def find(patterns)
22
+ # If no explicit patterns given, use patterns listed in config
23
+ patterns = @config.scss_files if patterns.empty?
24
+
25
+ matched_files = extract_files_from(patterns)
26
+ if matched_files.empty?
27
+ raise SCSSLint::Exceptions::NoFilesError,
28
+ "No SCSS files matched by the patterns: #{patterns.join(' ')}"
29
+ end
30
+
31
+ filtered_files = matched_files.reject { |file| @config.excluded_file?(file) }
32
+ if filtered_files.empty?
33
+ raise SCSSLint::Exceptions::AllFilesFilteredError,
34
+ "All files matched by the patterns [#{patterns.join(', ')}] " \
35
+ "were excluded by the patterns: [#{@config.exclude_patterns.join(', ')}]"
36
+ end
37
+
38
+ filtered_files
39
+ end
40
+
41
+ private
42
+
43
+ # @param list [Array]
44
+ def extract_files_from(list)
45
+ files = []
46
+
47
+ list.each do |file|
48
+ if File.directory?(file)
49
+ Find.find(file) do |f|
50
+ files << f if scssish_file?(f)
51
+ end
52
+ else
53
+ files << file # Otherwise include file as-is
54
+ end
55
+ end
56
+
57
+ files.uniq
58
+ end
59
+
60
+ # @param file [String]
61
+ # @return [true,false]
62
+ def scssish_file?(file)
63
+ return false unless FileTest.file?(file)
64
+
65
+ VALID_EXTENSIONS.include?(File.extname(file))
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,24 @@
1
+ module SCSSLint
2
+ # Stores information about a single problem that was detected by a [Linter].
3
+ class Lint
4
+ attr_reader :linter, :filename, :location, :description, :severity
5
+
6
+ # @param linter [SCSSLint::Linter]
7
+ # @param filename [String]
8
+ # @param location [SCSSLint::Location]
9
+ # @param description [String]
10
+ # @param severity [Symbol]
11
+ def initialize(linter, filename, location, description, severity = :warning)
12
+ @linter = linter
13
+ @filename = filename
14
+ @location = location
15
+ @description = description
16
+ @severity = severity
17
+ end
18
+
19
+ # @return [Boolean]
20
+ def error?
21
+ severity == :error
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,161 @@
1
+ module SCSSLint
2
+ # Defines common functionality available to all linters.
3
+ class Linter < Sass::Tree::Visitors::Base
4
+ include SelectorVisitor
5
+ include Utils
6
+
7
+ attr_reader :config, :engine, :lints
8
+
9
+ # Create a linter.
10
+ def initialize
11
+ @lints = []
12
+ end
13
+
14
+ # Run this linter against a parsed document with the given configuration,
15
+ # returning the lints that were found.
16
+ #
17
+ # @param engine [Engine]
18
+ # @param config [Config]
19
+ # @return [Array<Lint>]
20
+ def run(engine, config)
21
+ @lints = []
22
+ @config = config
23
+ @engine = engine
24
+ @comment_processor = ControlCommentProcessor.new(self)
25
+ visit(engine.tree)
26
+ @lints = @comment_processor.filter_lints(@lints)
27
+ end
28
+
29
+ # Return the human-friendly name of this linter as specified in the
30
+ # configuration file and in lint descriptions.
31
+ def name
32
+ self.class.name.split('::')[2..-1].join('::')
33
+ end
34
+
35
+ protected
36
+
37
+ # Helper for creating lint from a parse tree node
38
+ #
39
+ # @param node_or_line_or_location [Sass::Script::Tree::Node, Fixnum, SCSSLint::Location]
40
+ # @param message [String]
41
+ def add_lint(node_or_line_or_location, message)
42
+ @lints << Lint.new(self,
43
+ engine.filename,
44
+ extract_location(node_or_line_or_location),
45
+ message,
46
+ @config.fetch('severity', :warning).to_sym)
47
+ end
48
+
49
+ # Extract {SCSSLint::Location} from a {Sass::Source::Range}.
50
+ #
51
+ # @param range [Sass::Source::Range]
52
+ # @return [SCSSLint::Location]
53
+ def location_from_range(range) # rubocop:disable Metrics/AbcSize
54
+ length = if range.start_pos.line == range.end_pos.line
55
+ range.end_pos.offset - range.start_pos.offset
56
+ else
57
+ line_source = engine.lines[range.start_pos.line - 1]
58
+ line_source.length - range.start_pos.offset + 1
59
+ end
60
+
61
+ Location.new(range.start_pos.line, range.start_pos.offset, length)
62
+ end
63
+
64
+ # @param source_position [Sass::Source::Position]
65
+ # @param offset [Integer]
66
+ # @return [String] the character at the given [Sass::Source::Position]
67
+ def character_at(source_position, offset = 0)
68
+ actual_line = source_position.line - 1
69
+ actual_offset = source_position.offset + offset - 1
70
+
71
+ engine.lines[actual_line][actual_offset]
72
+ end
73
+
74
+ # Extracts the original source code given a range.
75
+ #
76
+ # @param source_range [Sass::Source::Range]
77
+ # @return [String] the original source code
78
+ def source_from_range(source_range) # rubocop:disable Metrics/AbcSize
79
+ current_line = source_range.start_pos.line - 1
80
+ last_line = source_range.end_pos.line - 1
81
+ start_pos = source_range.start_pos.offset - 1
82
+
83
+ if current_line == last_line
84
+ source = engine.lines[current_line][start_pos..(source_range.end_pos.offset - 1)]
85
+ else
86
+ source = engine.lines[current_line][start_pos..-1]
87
+ end
88
+
89
+ current_line += 1
90
+ while current_line < last_line
91
+ source += "#{engine.lines[current_line]}"
92
+ current_line += 1
93
+ end
94
+
95
+ if source_range.start_pos.line != source_range.end_pos.line
96
+ source += "#{(engine.lines[current_line] || '')[0...source_range.end_pos.offset]}"
97
+ end
98
+
99
+ source
100
+ end
101
+
102
+ # Returns whether a given node spans only a single line.
103
+ #
104
+ # @param node [Sass::Tree::Node]
105
+ # @return [true,false] whether the node spans a single line
106
+ def node_on_single_line?(node)
107
+ return if node.source_range.start_pos.line != node.source_range.end_pos.line
108
+
109
+ # The Sass parser reports an incorrect source range if the trailing curly
110
+ # brace is on the next line, e.g.
111
+ #
112
+ # p {
113
+ # }
114
+ #
115
+ # Since we don't want to count this as a single line node, check if the
116
+ # last character on the first line is an opening curly brace.
117
+ engine.lines[node.line - 1].strip[-1] != '{'
118
+ end
119
+
120
+ # Modified so we can also visit selectors in linters
121
+ #
122
+ # @param node [Sass::Tree::Node, Sass::Script::Tree::Node,
123
+ # Sass::Script::Value::Base]
124
+ def visit(node)
125
+ # Visit the selector of a rule if parsed rules are available
126
+ if node.is_a?(Sass::Tree::RuleNode) && node.parsed_rules
127
+ visit_selector(node.parsed_rules)
128
+ end
129
+
130
+ @comment_processor.before_node_visit(node)
131
+ super
132
+ @comment_processor.after_node_visit(node)
133
+ end
134
+
135
+ # Redefine so we can set the `node_parent` of each node
136
+ #
137
+ # @param parent [Sass::Tree::Node, Sass::Script::Tree::Node,
138
+ # Sass::Script::Value::Base]
139
+ def visit_children(parent)
140
+ parent.children.each do |child|
141
+ child.node_parent = parent
142
+ visit(child)
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def extract_location(node_or_line_or_location)
149
+ if node_or_line_or_location.is_a?(Location)
150
+ node_or_line_or_location
151
+ elsif node_or_line_or_location.respond_to?(:source_range) &&
152
+ node_or_line_or_location.source_range
153
+ location_from_range(node_or_line_or_location.source_range)
154
+ elsif node_or_line_or_location.respond_to?(:line)
155
+ Location.new(node_or_line_or_location.line)
156
+ else
157
+ Location.new(node_or_line_or_location)
158
+ end
159
+ end
160
+ end
161
+ end