Skip to content

Commit

Permalink
Adjust some operations for selector interpolation
Browse files Browse the repository at this point in the history
  • Loading branch information
srawlins committed Aug 25, 2015
1 parent fefdcb5 commit 24c1cdb
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 59 deletions.
174 changes: 115 additions & 59 deletions lib/scss_lint/linter/space_around_operator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,84 +3,140 @@ module SCSSLint
class Linter::SpaceAroundOperator < Linter
include LinterRegistry

def visit_script_operation(node) # rubocop:disable Metrics/AbcSize
source = normalize_source(source_from_range(node.source_range))
left_range = node.operand1.source_range
right_range = node.operand2.source_range

# We need to #chop at the end because an operation's operand1 _always_
# includes one character past the actual operand (which is either a
# whitespace character, or the first character of the operation).
left_source = normalize_source(source_from_range(left_range))
right_source = normalize_source(source_from_range(right_range))
operator_source = source_between(left_range, right_range)
left_source, operator_source = adjust_left_boundary(left_source, operator_source)

match = operator_source.match(/
def visit_script_operation(node)
operation_sources = OperationSources.new(node, self)
operation_sources.adjust_sources

# When an operation is found interpolated within something not a String
# (only selectors?), the source ranges are offset by two (probably not
# accounting for the `#{`. Slide everything to the left by 2, and maybe
# things will look sane this time.
unless operation_sources.operator_source =~ Sass::Script::Lexer::REGULAR_EXPRESSIONS[:op]
operation_sources.adjust_for_interpolation
operation_sources.adjust_sources
end

check(node, operation_sources)

yield
end

def source_fm_range(range)
source_from_range(range)
end

private

def check(node, operation_sources)
match = operation_sources.operator_source.match(/
(?<left_space>\s*)
(?<operator>\S+)
(?<right_space>\s*)
/x)

if config['style'] == 'one_space'
if match[:left_space] != ' ' || match[:right_space] != ' '
add_lint(node, SPACE_MSG % [source, left_source, match[:operator], right_source])
add_lint(node, operation_sources.space_msg(match[:operator]))
end
elsif match[:left_space] != '' || match[:right_space] != ''
add_lint(node, NO_SPACE_MSG % [source, left_source, match[:operator], right_source])
add_lint(node, operation_sources.no_space_msg(match[:operator]))
end

yield
end

private
# A helper class for storing and adjusting the sources of the different
# components of an Operation node.
class OperationSources
attr_reader :operator_source

SPACE_MSG = '`%s` should be written with a single space on each side of ' \
'the operator: `%s %s %s`'
NO_SPACE_MSG = '`%s` should be written without spaces around the ' \
'operator: `%s%s%s`'

def source_between(range1, range2)
# We don't want to add 1 to range1.end_pos.offset for the same reason as
# the #chop comment above.
between_start = Sass::Source::Position.new(
range1.end_pos.line,
range1.end_pos.offset,
)
between_end = Sass::Source::Position.new(
range2.start_pos.line,
range2.start_pos.offset - 1,
)

source_from_range(Sass::Source::Range.new(between_start,
between_end,
range1.file,
range1.importer))
end
def initialize(node, linter)
@node = node
@linter = linter
@source = normalize_source(@linter.source_fm_range(@node.source_range))
@left_range = @node.operand1.source_range
@right_range = @node.operand2.source_range
end

# Removes trailing parentheses and compacts newlines into a single space
def normalize_source(source)
source.chop.gsub(/\s*\n\s*/, ' ')
end
def adjust_sources
# We need to #chop at the end because an operation's operand1 _always_
# includes one character past the actual operand (which is either a
# whitespace character, or the first character of the operation).
@left_source = normalize_source(@linter.source_fm_range(@left_range))
@right_source = normalize_source(@linter.source_fm_range(@right_range))
@operator_source = calculate_operator_source
adjust_left_boundary
end

def adjust_left_boundary(left, operator)
# If the left operand is wrapped in parentheses, any right parens end up
# in the operator source. Here, we move them into the left operand
# source, which is awkward in any messaging, but it works.
if match = operator.match(/^(\s*\))+/)
left += match[0]
operator = operator[match.end(0)..-1]
def adjust_for_interpolation
@source = normalize_source(
@linter.source_fm_range(slide_to_the_left(@node.source_range)))
@left_range = slide_to_the_left(@node.operand1.source_range)
@right_range = slide_to_the_left(@node.operand2.source_range)
end

# If the left operand is a nested operation, Sass includes any whitespace
# before the (outer) operator in the left operator's source_range's
# end_pos, which is not the case with simple, non-operation operands.
if match = left.match(/\s+$/)
left = left[0..match.begin(0)]
operator = match[0] + operator
def space_msg(operator)
SPACE_MSG % [@source, @left_source, operator, @right_source]
end

[left, operator]
def no_space_msg(operator)
NO_SPACE_MSG % [@source, @left_source, operator, @right_source]
end

private

SPACE_MSG = '`%s` should be written with a single space on each side of ' \
'the operator: `%s %s %s`'

NO_SPACE_MSG = '`%s` should be written without spaces around the ' \
'operator: `%s%s%s`'

def calculate_operator_source
# We don't want to add 1 to range1.end_pos.offset for the same reason as
# the #chop comment above.
between_start = Sass::Source::Position.new(
@left_range.end_pos.line,
@left_range.end_pos.offset,
)
between_end = Sass::Source::Position.new(
@right_range.start_pos.line,
@right_range.start_pos.offset - 1,
)

@linter.source_fm_range(Sass::Source::Range.new(between_start,
between_end,
@left_range.file,
@left_range.importer))
end

def adjust_left_boundary
# If the left operand is wrapped in parentheses, any right parens end up
# in the operator source. Here, we move them into the left operand
# source, which is awkward in any messaging, but it works.
if match = @operator_source.match(/^(\s*\))+/)
@left_source += match[0]
@operator_source = @operator_source[match.end(0)..-1]
end

# If the left operand is a nested operation, Sass includes any whitespace
# before the (outer) operator in the left operator's source_range's
# end_pos, which is not the case with simple, non-operation operands.
if match = @left_source.match(/\s+$/)
@left_source = @left_source[0..match.begin(0)]
@operator_source = match[0] + @operator_source
end

[@left_source, @operator_source]
end

# Removes trailing parentheses and compacts newlines into a single space
def normalize_source(source)
source.chop.gsub(/\s*\n\s*/, ' ')
end

def slide_to_the_left(range)
start_pos = Sass::Source::Position.new(range.start_pos.line, range.start_pos.offset - 2)
end_pos = Sass::Source::Position.new(range.end_pos.line, range.end_pos.offset - 2)
Sass::Source::Range.new(start_pos, end_pos, range.file, range.importer)
end
end
end
end
25 changes: 25 additions & 0 deletions spec/scss_lint/linter/space_around_operator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,30 @@
it { should report_lint line: 2 }
end

context 'when a selector contains an interpolated infix operator, well spaced' do
let(:scss) { <<-SCSS }
@for $i from 1 through 25 {
.progress-bar[aria-valuenow="\#{$i / 25 * 100}"] {
opacity: 0;
}
}
SCSS

it { should_not report_lint }
end

context 'when a selector contains an interpolated infix operator, badly spaced' do
let(:scss) { <<-SCSS }
@for $i from 1 through 25 {
.progress-bar[aria-valuenow="\#{$i/25*100}"] {
opacity: 0;
}
}
SCSS

it { should report_lint line: 2, count: 2 }
end

context 'when values with non-evaluated operations exist' do
let(:scss) { <<-SCSS }
$my-variable: 10px;
Expand Down Expand Up @@ -207,6 +231,7 @@
it { should report_lint line: 6 }
end
end

context 'when one space is preferred' do
let(:style) { 'no_space' }

Expand Down

0 comments on commit 24c1cdb

Please sign in to comment.