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.
- 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
|