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

  sortable_prop_info = sortable_props
    .map do |child|
      name = child.name.join
      /^(?<vendor>-\w+(-osx)?-)?(?<property>.+)/ =~ name
      { name: name, vendor: vendor, property: property, node: child }
    end

  if sortable_props.any?
    check_sort_order(sortable_prop_info)
    check_group_separation(sortable_prop_info) if @group
  end

  yield # Continue linting children
end
Also aliased as: visit_media, visit_mixin, visit_rule
visit_if(node, &block) click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 40
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_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 51
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 81
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 69
def check_sort_order(sortable_prop_info)
  sorted_props = sortable_prop_info
    .sort { |a, b| compare_properties(a, b) }

  sorted_props.each_with_index do |prop, index|
    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 139
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 127
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 115
def compare_properties(a, b)
  if a[:property] == b[:property]
    compare_by_vendor(a, b)
  else
    if @preferred_order
      compare_by_order(a, b, @preferred_order)
    else
      a[:property] <=> b[:property]
    end
  end
end
extract_preferred_order_from_config() click to toggle source
# File lib/scss_lint/linter/property_sort_order.rb, line 144
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 172
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 184
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 180
def preset_order?
  config['order'].is_a?(String)
end