class SCSSLint::Linter::PropertySortOrder

Checks the declaration order of properties.

Public Instance Methods

check_order(node) { || ... } click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 16
def check_order(node)
  sortable_props = node.children.select do |child|
    child.is_a?(Sass::Tree::PropNode) && !ignore_property?(child)
  end

  if sortable_props.count >= config.fetch('min_properties', 2)
    sortable_prop_info =
      sortable_props.map do |child|
        name = child.name.join
        /^(?<vendor>-\w+(-osx)?-)?(?<property>.+)/ =~ name
        { name: name, vendor: vendor, property: "#{@nested_under}#{property}", node: child }
      end

    check_sort_order(sortable_prop_info)
    check_group_separation(sortable_prop_info) if @group
  end

  yield # Continue linting children
end
visit_if(node, &block) click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 49
def visit_if(node, &block)
  check_order(node, &block)
  visit(node.else) if node.else
end
visit_media(node)
Alias for: check_order
visit_mixin(node)
Alias for: check_order
visit_prop(node)
Alias for: check_order
visit_root(_node) { || ... } click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 6
def visit_root(_node)
  @preferred_order = extract_preferred_order_from_config

  if @preferred_order && config['separate_groups']
    @group = assign_groups(@preferred_order)
  end

  yield # Continue linting children
end
visit_rule(node)
Alias for: check_order

Private Instance Methods

assign_groups(order) click to toggle source

When enforcing whether a blank line should separate “groups” of properties, we need to assign those properties to group numbers so we can quickly tell traversing from one property to the other that a blank line is required (since the group number would change).

# File lib/scss_lint/linter/property_sort_order.rb, line 60
def assign_groups(order)
  group_number = 0
  last_was_empty = false

  order.each_with_object({}) do |property, group|
    # A gap indicates the start of the next group
    if property.nil? || property.strip.empty?
      group_number += 1 unless last_was_empty # Treat multiple gaps as single gap
      last_was_empty = true
      next
    end

    last_was_empty = false

    group[property] = group_number
  end
end
check_group_separation(sortable_prop_info) click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 94
def check_group_separation(sortable_prop_info) # rubocop:disable AbcSize
  group_number = @group[sortable_prop_info.first[:property]]

  sortable_prop_info[0..-2].zip(sortable_prop_info[1..-1]).each do |first, second|
    next unless @group[second[:property]] != group_number

    # We're now in the next group
    group_number = @group[second[:property]]

    # The group number has changed, so ensure this property is separated
    # from the previous property by at least a line (could be a comment,
    # we don't care, but at least one line that isn't another property).
    next if first[:node].line < second[:node].line - 1

    add_lint second[:node], "Property #{second[:name]} should be "                                  'separated from the previous group of '                                  "properties ending with #{first[:name]}"
  end
end
check_sort_order(sortable_prop_info) click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 78
def check_sort_order(sortable_prop_info)
  sortable_prop_info = sortable_prop_info.uniq { |item| item[:name] }
  sorted_props = sortable_prop_info.sort { |a, b| compare_properties(a, b) }

  sorted_props.each_with_index do |prop, index|
    # Once we reach the portion of the list with unspecified properties, we
    # can stop checking since we don't care about order after that point
    break unless specified_property?(prop[:property])

    next unless prop != sortable_prop_info[index]

    add_lint(sortable_prop_info[index][:node], lint_message(sorted_props))
    break
  end
end
compare_by_order(a, b, order) click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 150
def compare_by_order(a, b, order)
  (order.index(a[:property]) || Float::INFINITY) <=>
    (order.index(b[:property]) || Float::INFINITY)
end
compare_by_vendor(a, b) click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 138
def compare_by_vendor(a, b)
  if a[:vendor] && b[:vendor]
    a[:vendor] <=> b[:vendor]
  elsif a[:vendor]
    -1
  elsif b[:vendor]
    1
  else
    0
  end
end
compare_properties(a, b) click to toggle source

Compares two properties which can contain a vendor prefix. It allows for a sort order like:

p {
  border: ...
  -moz-border-radius: ...
  -o-border-radius: ...
  -webkit-border-radius: ...
  border-radius: ...
  color: ...
}

…where vendor-prefixed properties come before the standard property, and are ordered amongst themselves by vendor prefix.

# File lib/scss_lint/linter/property_sort_order.rb, line 128
def compare_properties(a, b)
  if a[:property] == b[:property]
    compare_by_vendor(a, b)
  elsif @preferred_order
    compare_by_order(a, b, @preferred_order)
  else
    a[:property] <=> b[:property]
  end
end
extract_preferred_order_from_config() click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 155
def extract_preferred_order_from_config
  case config['order']
  when nil
    nil # No custom order specified
  when Array
    config['order']
  when String
    begin
      file = File.open(File.join(SCSS_LINT_DATA,
                                 'property-sort-orders',
                                 "#{config['order']}.txt"))
      file.read.split("\n").reject { |line| line =~ /^(#|\s*$)/ }
    rescue Errno::ENOENT
      raise SCSSLint::Exceptions::LinterError,
            "Preset property sort order '#{config['order']}' does not exist"
    end
  else
    raise SCSSLint::Exceptions::LinterError,
          'Invalid property sort order specified -- must be the name of a '               'preset or an array of strings'
  end
end
ignore_property?(prop_node) click to toggle source

Return whether to ignore a property in the sort order.

This includes:

  • properties containing interpolation

  • properties not explicitly defined in the sort order (if ignore_unspecified is set)

# File lib/scss_lint/linter/property_sort_order.rb, line 183
def ignore_property?(prop_node)
  return true if prop_node.name.any? { |part| !part.is_a?(String) }

  config['ignore_unspecified'] &&
    @preferred_order &&
    !@preferred_order.include?(prop_node.name.join)
end
lint_message(sortable_prop_info) click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 199
def lint_message(sortable_prop_info)
  props = sortable_prop_info.map { |prop| prop[:name] }.join(', ')
  "Properties should be ordered #{props}"
end
preset_order?() click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 195
def preset_order?
  config['order'].is_a?(String)
end
specified_property?(prop_name) click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 191
def specified_property?(prop_name)
  !@preferred_order || @preferred_order.include?(prop_name)
end