forked from sds/scss-lint
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rewrite SpaceBetweenParens linter to reduce false positives
- Loading branch information
Showing
3 changed files
with
310 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,112 @@ | ||
module SCSSLint | ||
# rubocop:disable Metrics/AbcSize | ||
|
||
# Checks for the presence of spaces between parentheses. | ||
class Linter::SpaceBetweenParens < Linter | ||
include LinterRegistry | ||
|
||
def visit_root(_node) | ||
def check_node(node) | ||
check(node, source_from_range(node.source_range)) | ||
yield | ||
end | ||
|
||
alias_method :visit_atroot, :check_node | ||
alias_method :visit_cssimport, :check_node | ||
alias_method :visit_function, :check_node | ||
alias_method :visit_media, :check_node | ||
alias_method :visit_mixindef, :check_node | ||
alias_method :visit_mixin, :check_node | ||
alias_method :visit_script_funcall, :check_node | ||
|
||
def feel_for_parens_and_check_node(node) | ||
source = feel_for_enclosing_parens(node) | ||
check(node, source) | ||
yield | ||
end | ||
|
||
alias_method :visit_script_listliteral, :feel_for_parens_and_check_node | ||
alias_method :visit_script_mapliteral, :feel_for_parens_and_check_node | ||
alias_method :visit_script_operation, :feel_for_parens_and_check_node | ||
alias_method :visit_script_string, :feel_for_parens_and_check_node | ||
|
||
private | ||
|
||
TRAILING_WHITESPACE = /\s*$/ | ||
|
||
def check(node, source) # rubocop:disable Metrics/MethodLength | ||
@spaces = config['spaces'] | ||
source = trim_right_paren(source) | ||
return if source.count('(') != source.count(')') | ||
source.scan(/ | ||
\( | ||
(?<left>\s*) | ||
(?<contents>.*) | ||
(?<right>\s*) | ||
\) | ||
/x) do |left, contents, right| | ||
right = contents.match(TRAILING_WHITESPACE)[0] + right | ||
contents.gsub(TRAILING_WHITESPACE, '') | ||
|
||
# We don't lint on multiline parenthetical source. | ||
break if (left + contents + right).include? "\n" | ||
|
||
engine.lines.each_with_index do |line, index| | ||
line.gsub(%r{((//|/\*).*$)}, '').scan(/ | ||
(^(\t|\s)*\))? # Capture leading spaces and tabs followed by a `)` | ||
( | ||
\([ ]*(?!$) # Find `( ` as long as its not EOL ) | ||
| | ||
[ ]*\) | ||
)? | ||
/x) do |match| | ||
check(match[2], index) if match[2] | ||
if contents.empty? | ||
# If we're looking at empty parens (like `()`, `( )`, `( )`, etc.), | ||
# only report a possible lint on the left side. | ||
right = ' ' * @spaces | ||
end | ||
|
||
if left != ' ' * @spaces | ||
message = "#{expected_spaces} after `(` instead of `#{left}`" | ||
add_lint(node, message) | ||
end | ||
|
||
if right != ' ' * @spaces | ||
message = "#{expected_spaces} before `)` instead of `#{right}`" | ||
add_lint(node, message) | ||
end | ||
end | ||
yield | ||
end | ||
|
||
private | ||
# An expression enclosed in parens will include or not include each paren, depending | ||
# on whitespace. Here we feel out for enclosing parens, and return them as the new | ||
# source for the node. | ||
def feel_for_enclosing_parens(node) # rubocop:disable Metrics/CyclomaticComplexity | ||
range = node.source_range | ||
original_source = source_from_range(range) | ||
left_offset = -1 | ||
right_offset = 0 | ||
|
||
if original_source[-1] != ')' | ||
right_offset += 1 while character_at(range.end_pos, right_offset) =~ /\s/ | ||
|
||
def check(str, index) | ||
spaces = str.count ' ' | ||
return if spaces == @spaces | ||
return original_source if character_at(range.end_pos, right_offset) != ')' | ||
end | ||
|
||
# At this point, we know that we're wrapped on the right by a ')'. | ||
# Are we wrapped on the left by a '('? | ||
left_offset -= 1 while character_at(range.start_pos, left_offset) =~ /\s/ | ||
return original_source if character_at(range.start_pos, left_offset) != '(' | ||
|
||
# At this point, we know we're wrapped on both sides by parens. However, | ||
# those parens may be part of a parent function call. We don't care about | ||
# such parens. This depends on whether the preceding character is part of | ||
# a function name. | ||
return original_source if character_at(range.start_pos, left_offset - 1) =~ /[A-Za-z0-9_]/ | ||
|
||
range.start_pos.offset += left_offset | ||
range.end_pos.offset += right_offset | ||
source_from_range(range) | ||
end | ||
|
||
# An unrelated right paren will sneak into the source of a node if there is no | ||
# whitespace between the node and the right paren. | ||
def trim_right_paren(source) | ||
(source.count(')') == source.count('(') + 1) ? source[0..-2] : source | ||
end | ||
|
||
location = Location.new(index + 1) | ||
message = "Expected #{pluralize(@spaces, 'space')} " \ | ||
"between parentheses instead of #{spaces}" | ||
add_lint(location, message) | ||
def expected_spaces | ||
"Expected #{pluralize(@spaces, 'space')}" | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters