Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 42 additions & 7 deletions lib/super_slim/linter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ def each_tag(&) = tag_visits.each(&)
# The layout rules: what `super-slim format` rewrites.
LAYOUT_RULE_NAMES = %w[Indentation Tab TrailingWhitespace EmptyLines ControlStatementSpacing RedundantDiv].freeze
GLOB_OPTIONS = %w[include exclude].freeze
# What the default of a rule's option asks of the value that replaces it.
VALUE_KIND_BY_DEFAULT_CLASS = {
TrueClass => ["true or false", [TrueClass, FalseClass]],
FalseClass => ["true or false", [TrueClass, FalseClass]],
Integer => ["a whole number", [Integer]],
# Left empty under a preset, `classes:` is nil.
Hash => ["a mapping", [Hash, NilClass]],
Array => ["a list", [Array]]
}.freeze

def self.load(directory = Dir.pwd)
new(configuration(directory).fetch("linters", {}), directory)
Expand All @@ -88,22 +97,48 @@ def self.configuration(directory = Dir.pwd)
configuration
end

# A value of the wrong shape would fail later, inside a rule, with a NoMethodError naming none of this file.
# A value of the wrong shape would fail later, inside a rule or the formatter, on every template and with an error
# naming none of this file: `max: "120"` compares a number with text.
def self.check_configuration(configuration)
raise Error, "#{CONFIG_FILE}: expected a mapping of settings" unless configuration.is_a?(Hash)

%w[linters format fmt].each do |key|
raise Error, "#{CONFIG_FILE}: `#{key}` must be a mapping" unless configuration.fetch(key, {}).is_a?(Hash)
end
configuration.fetch("linters", {}).each do |rule_name, options|
raise Error, "#{CONFIG_FILE}: `linters: #{rule_name}` must be a mapping of options" unless options.is_a?(Hash)
raise Error, "#{CONFIG_FILE}: `tailwind_classes` must be true or false" unless [true, false, nil].include?(configuration["tailwind_classes"])

GLOB_OPTIONS.each do |key|
globs = Array(options[key])
raise Error, "#{CONFIG_FILE}: `#{rule_name}: #{key}` must be a list of globs" unless globs.all?(String)
end
configuration.fetch("linters", {}).each { |rule_name, options| check_rule_options(rule_name, options) }
configuration.slice("format", "fmt").each { |key, options| check_format_options(key, options) }
end

def self.check_rule_options(rule_name, options)
raise Error, "#{CONFIG_FILE}: `linters: #{rule_name}` must be a mapping of options" unless options.is_a?(Hash)

GLOB_OPTIONS.each do |key|
globs = Array(options[key])
raise Error, "#{CONFIG_FILE}: `#{rule_name}: #{key}` must be a list of globs" unless globs.all?(String)
end
rule_class = Rules.all.find { _1.rule_name == rule_name } or return
rule_class::DEFAULTS.each do |key, default|
next unless options.key?(key.to_s)

kind, classes = VALUE_KIND_BY_DEFAULT_CLASS.fetch(default.class)
raise Error, "#{CONFIG_FILE}: `#{rule_name}: #{key}` must be #{kind}" unless classes.any? { options[key.to_s].is_a?(_1) }
end
end
private_class_method :check_rule_options

def self.check_format_options(key, options)
max_line_length = options["max_line_length"]
wraps_nothing_or_at_a_length = max_line_length.nil? || (max_line_length.is_a?(Integer) && max_line_length.positive?)
raise Error, "#{CONFIG_FILE}: `#{key}: max_line_length` must be a whole number above 0" unless wraps_nothing_or_at_a_length

delimiters = Formatter::LineWrapper::OPENING_BY_ATTRIBUTE_DELIMITERS.keys
return if !options.key?("attribute_delimiters") || delimiters.include?(options["attribute_delimiters"])

raise Error, "#{CONFIG_FILE}: `#{key}: attribute_delimiters` must be one of #{delimiters.join(", ")}"
end
private_class_method :check_format_options

# include: and exclude: globs match paths relative to `root`, the directory of .super-slim.yml.
def initialize(options_by_rule_name = {}, root = Dir.pwd)
Expand Down
17 changes: 17 additions & 0 deletions test/cli_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ def test_a_configuration_of_the_wrong_shape_fails_with_a_usage_error
{
"linters: 5\n" => ".super-slim.yml: `linters` must be a mapping",
"linters:\n LineLength:\n include: 3\n" => ".super-slim.yml: `LineLength: include` must be a list of globs",
"linters:\n LineLength:\n max: \"120\"\n" => ".super-slim.yml: `LineLength: max` must be a whole number",
"linters:\n LineLength:\n enabled: \"false\"\n" => ".super-slim.yml: `LineLength: enabled` must be true or false",
"linters:\n DeprecatedClasses:\n classes: [close]\n" => ".super-slim.yml: `DeprecatedClasses: classes` must be a mapping",
"linters:\n EmbeddedEngines:\n forbidden_engines: css\n" => ".super-slim.yml: `EmbeddedEngines: forbidden_engines` must be a list",
"format:\n max_line_length: \"120\"\n" => ".super-slim.yml: `format: max_line_length` must be a whole number above 0",
"fmt:\n max_line_length: 0\n" => ".super-slim.yml: `fmt: max_line_length` must be a whole number above 0",
"format:\n attribute_delimiters: \"<>\"\n" => ".super-slim.yml: `format: attribute_delimiters` must be one of (), [], {}",
"tailwind_classes: maybe\n" => ".super-slim.yml: `tailwind_classes` must be true or false",
"x: !ruby/object:Object {}\n" => "Tried to load unspecified class: Object"
}.each do |configuration, message|
in_templates("p Hello\n") do |directory|
Expand All @@ -109,6 +117,15 @@ def test_a_configuration_of_the_wrong_shape_fails_with_a_usage_error
end
end

def test_a_preset_table_and_a_line_length_left_empty_are_no_error
in_templates("p Hello\n") do |directory|
File.write(File.join(directory, ".super-slim.yml"), "linters:\n DeprecatedAttributes:\n preset: bootstrap5\n attributes:\nformat:\n max_line_length:\n")

assert_equal [0, ""], run_cli("lint", ".")
assert_equal [0, ""], run_cli("format", "--check", ".")
end
end

def test_include_and_exclude_globs_match_paths_from_the_configuration_directory
in_templates("p style=\"color: red\" Hello\n") do |directory|
FileUtils.mkdir_p(File.join(directory, "mailers"))
Expand Down
Loading