scss_lint 0.38.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/scss-lint +6 -0
- data/config/default.yml +205 -0
- data/data/prefixed-identifiers/base.txt +107 -0
- data/data/prefixed-identifiers/bourbon.txt +71 -0
- data/data/properties.txt +477 -0
- data/data/property-sort-orders/concentric.txt +134 -0
- data/data/property-sort-orders/recess.txt +149 -0
- data/data/property-sort-orders/smacss.txt +137 -0
- data/lib/scss_lint.rb +31 -0
- data/lib/scss_lint/cli.rb +215 -0
- data/lib/scss_lint/config.rb +251 -0
- data/lib/scss_lint/constants.rb +8 -0
- data/lib/scss_lint/control_comment_processor.rb +126 -0
- data/lib/scss_lint/engine.rb +56 -0
- data/lib/scss_lint/exceptions.rb +21 -0
- data/lib/scss_lint/file_finder.rb +68 -0
- data/lib/scss_lint/lint.rb +24 -0
- data/lib/scss_lint/linter.rb +161 -0
- data/lib/scss_lint/linter/bang_format.rb +52 -0
- data/lib/scss_lint/linter/border_zero.rb +39 -0
- data/lib/scss_lint/linter/color_keyword.rb +32 -0
- data/lib/scss_lint/linter/color_variable.rb +60 -0
- data/lib/scss_lint/linter/comment.rb +21 -0
- data/lib/scss_lint/linter/compass.rb +7 -0
- data/lib/scss_lint/linter/compass/property_with_mixin.rb +47 -0
- data/lib/scss_lint/linter/debug_statement.rb +10 -0
- data/lib/scss_lint/linter/declaration_order.rb +71 -0
- data/lib/scss_lint/linter/duplicate_property.rb +58 -0
- data/lib/scss_lint/linter/else_placement.rb +48 -0
- data/lib/scss_lint/linter/empty_line_between_blocks.rb +85 -0
- data/lib/scss_lint/linter/empty_rule.rb +11 -0
- data/lib/scss_lint/linter/final_newline.rb +20 -0
- data/lib/scss_lint/linter/hex_length.rb +56 -0
- data/lib/scss_lint/linter/hex_notation.rb +38 -0
- data/lib/scss_lint/linter/hex_validation.rb +23 -0
- data/lib/scss_lint/linter/id_selector.rb +10 -0
- data/lib/scss_lint/linter/import_path.rb +62 -0
- data/lib/scss_lint/linter/important_rule.rb +12 -0
- data/lib/scss_lint/linter/indentation.rb +197 -0
- data/lib/scss_lint/linter/leading_zero.rb +49 -0
- data/lib/scss_lint/linter/mergeable_selector.rb +60 -0
- data/lib/scss_lint/linter/name_format.rb +117 -0
- data/lib/scss_lint/linter/nesting_depth.rb +24 -0
- data/lib/scss_lint/linter/placeholder_in_extend.rb +22 -0
- data/lib/scss_lint/linter/property_count.rb +44 -0
- data/lib/scss_lint/linter/property_sort_order.rb +198 -0
- data/lib/scss_lint/linter/property_spelling.rb +49 -0
- data/lib/scss_lint/linter/property_units.rb +59 -0
- data/lib/scss_lint/linter/qualifying_element.rb +42 -0
- data/lib/scss_lint/linter/selector_depth.rb +64 -0
- data/lib/scss_lint/linter/selector_format.rb +102 -0
- data/lib/scss_lint/linter/shorthand.rb +139 -0
- data/lib/scss_lint/linter/single_line_per_property.rb +59 -0
- data/lib/scss_lint/linter/single_line_per_selector.rb +35 -0
- data/lib/scss_lint/linter/space_after_comma.rb +110 -0
- data/lib/scss_lint/linter/space_after_property_colon.rb +92 -0
- data/lib/scss_lint/linter/space_after_property_name.rb +27 -0
- data/lib/scss_lint/linter/space_before_brace.rb +72 -0
- data/lib/scss_lint/linter/space_between_parens.rb +35 -0
- data/lib/scss_lint/linter/string_quotes.rb +94 -0
- data/lib/scss_lint/linter/trailing_semicolon.rb +67 -0
- data/lib/scss_lint/linter/trailing_zero.rb +41 -0
- data/lib/scss_lint/linter/unnecessary_mantissa.rb +42 -0
- data/lib/scss_lint/linter/unnecessary_parent_reference.rb +49 -0
- data/lib/scss_lint/linter/url_format.rb +56 -0
- data/lib/scss_lint/linter/url_quotes.rb +27 -0
- data/lib/scss_lint/linter/variable_for_property.rb +30 -0
- data/lib/scss_lint/linter/vendor_prefix.rb +64 -0
- data/lib/scss_lint/linter/zero_unit.rb +39 -0
- data/lib/scss_lint/linter_registry.rb +26 -0
- data/lib/scss_lint/location.rb +38 -0
- data/lib/scss_lint/options.rb +109 -0
- data/lib/scss_lint/rake_task.rb +106 -0
- data/lib/scss_lint/reporter.rb +18 -0
- data/lib/scss_lint/reporter/config_reporter.rb +26 -0
- data/lib/scss_lint/reporter/default_reporter.rb +27 -0
- data/lib/scss_lint/reporter/files_reporter.rb +8 -0
- data/lib/scss_lint/reporter/json_reporter.rb +30 -0
- data/lib/scss_lint/reporter/xml_reporter.rb +33 -0
- data/lib/scss_lint/runner.rb +51 -0
- data/lib/scss_lint/sass/script.rb +78 -0
- data/lib/scss_lint/sass/tree.rb +168 -0
- data/lib/scss_lint/selector_visitor.rb +34 -0
- data/lib/scss_lint/utils.rb +112 -0
- data/lib/scss_lint/version.rb +4 -0
- data/spec/scss_lint/cli_spec.rb +177 -0
- data/spec/scss_lint/config_spec.rb +253 -0
- data/spec/scss_lint/engine_spec.rb +24 -0
- data/spec/scss_lint/file_finder_spec.rb +134 -0
- data/spec/scss_lint/linter/bang_format_spec.rb +121 -0
- data/spec/scss_lint/linter/border_zero_spec.rb +118 -0
- data/spec/scss_lint/linter/color_keyword_spec.rb +83 -0
- data/spec/scss_lint/linter/color_variable_spec.rb +155 -0
- data/spec/scss_lint/linter/comment_spec.rb +79 -0
- data/spec/scss_lint/linter/compass/property_with_mixin_spec.rb +55 -0
- data/spec/scss_lint/linter/debug_statement_spec.rb +21 -0
- data/spec/scss_lint/linter/declaration_order_spec.rb +575 -0
- data/spec/scss_lint/linter/duplicate_property_spec.rb +189 -0
- data/spec/scss_lint/linter/else_placement_spec.rb +106 -0
- data/spec/scss_lint/linter/empty_line_between_blocks_spec.rb +276 -0
- data/spec/scss_lint/linter/empty_rule_spec.rb +27 -0
- data/spec/scss_lint/linter/final_newline_spec.rb +49 -0
- data/spec/scss_lint/linter/hex_length_spec.rb +104 -0
- data/spec/scss_lint/linter/hex_notation_spec.rb +104 -0
- data/spec/scss_lint/linter/hex_validation_spec.rb +40 -0
- data/spec/scss_lint/linter/id_selector_spec.rb +62 -0
- data/spec/scss_lint/linter/import_path_spec.rb +300 -0
- data/spec/scss_lint/linter/important_rule_spec.rb +43 -0
- data/spec/scss_lint/linter/indentation_spec.rb +347 -0
- data/spec/scss_lint/linter/leading_zero_spec.rb +233 -0
- data/spec/scss_lint/linter/mergeable_selector_spec.rb +283 -0
- data/spec/scss_lint/linter/name_format_spec.rb +282 -0
- data/spec/scss_lint/linter/nesting_depth_spec.rb +114 -0
- data/spec/scss_lint/linter/placeholder_in_extend_spec.rb +63 -0
- data/spec/scss_lint/linter/property_count_spec.rb +104 -0
- data/spec/scss_lint/linter/property_sort_order_spec.rb +482 -0
- data/spec/scss_lint/linter/property_spelling_spec.rb +84 -0
- data/spec/scss_lint/linter/property_units_spec.rb +229 -0
- data/spec/scss_lint/linter/qualifying_element_spec.rb +125 -0
- data/spec/scss_lint/linter/selector_depth_spec.rb +159 -0
- data/spec/scss_lint/linter/selector_format_spec.rb +632 -0
- data/spec/scss_lint/linter/shorthand_spec.rb +198 -0
- data/spec/scss_lint/linter/single_line_per_property_spec.rb +73 -0
- data/spec/scss_lint/linter/single_line_per_selector_spec.rb +130 -0
- data/spec/scss_lint/linter/space_after_comma_spec.rb +332 -0
- data/spec/scss_lint/linter/space_after_property_colon_spec.rb +373 -0
- data/spec/scss_lint/linter/space_after_property_name_spec.rb +37 -0
- data/spec/scss_lint/linter/space_before_brace_spec.rb +829 -0
- data/spec/scss_lint/linter/space_between_parens_spec.rb +263 -0
- data/spec/scss_lint/linter/string_quotes_spec.rb +335 -0
- data/spec/scss_lint/linter/trailing_semicolon_spec.rb +304 -0
- data/spec/scss_lint/linter/trailing_zero_spec.rb +176 -0
- data/spec/scss_lint/linter/unnecessary_mantissa_spec.rb +67 -0
- data/spec/scss_lint/linter/unnecessary_parent_reference_spec.rb +98 -0
- data/spec/scss_lint/linter/url_format_spec.rb +55 -0
- data/spec/scss_lint/linter/url_quotes_spec.rb +73 -0
- data/spec/scss_lint/linter/variable_for_property_spec.rb +145 -0
- data/spec/scss_lint/linter/vendor_prefix_spec.rb +371 -0
- data/spec/scss_lint/linter/zero_unit_spec.rb +113 -0
- data/spec/scss_lint/linter_registry_spec.rb +50 -0
- data/spec/scss_lint/linter_spec.rb +292 -0
- data/spec/scss_lint/location_spec.rb +42 -0
- data/spec/scss_lint/options_spec.rb +34 -0
- data/spec/scss_lint/rake_task_spec.rb +43 -0
- data/spec/scss_lint/reporter/config_reporter_spec.rb +42 -0
- data/spec/scss_lint/reporter/default_reporter_spec.rb +73 -0
- data/spec/scss_lint/reporter/files_reporter_spec.rb +38 -0
- data/spec/scss_lint/reporter/json_reporter_spec.rb +96 -0
- data/spec/scss_lint/reporter/xml_reporter_spec.rb +103 -0
- data/spec/scss_lint/reporter_spec.rb +11 -0
- data/spec/scss_lint/runner_spec.rb +123 -0
- data/spec/scss_lint/selector_visitor_spec.rb +264 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/support/isolated_environment.rb +25 -0
- data/spec/support/matchers/report_lint.rb +48 -0
- 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
|