scss_lint 0.38.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 (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