From 70938d19a1243eb858b471d3303d1e107f5b8744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Joubert?= <14306+czj@users.noreply.github.com> Date: Wed, 11 Nov 2026 10:55:53 +0100 Subject: [PATCH] A name .super-slim.yml misspells is an error, not ignored A rule, an option or a setting the linter does not know was skipped in silence: the limit it meant to raise stayed, the rule it meant to turn off stayed on. Unknown names stop lint and format with a usage error that lists the known ones. Assisted-by: Claude Fable 5 [Claude Code] --- README.md | 3 ++- lib/super_slim/linter.rb | 18 +++++++++++++++++- test/cli_test.rb | 16 ++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 81393ad..528bf98 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,8 @@ linters: ``` Every rule accepts `include:` and `exclude:`, lists of glob patterns matched against paths relative to -`.super-slim.yml` (`**/` matches any number of directories). +`.super-slim.yml` (`**/` matches any number of directories). A rule, an option or a setting the file misspells +stops `lint` and `format` with an error that names it, rather than being ignored. A comment silences rules on the next tag or code line, blank lines and comments skipped, and on the lines it goes on over. The linter reports a rule name it doesn't know: diff --git a/lib/super_slim/linter.rb b/lib/super_slim/linter.rb index 9541295..d24b2aa 100644 --- a/lib/super_slim/linter.rb +++ b/lib/super_slim/linter.rb @@ -76,6 +76,8 @@ 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 + SETTINGS = %w[linters format fmt tailwind_classes].freeze + FORMAT_SETTINGS = %w[max_line_length attribute_delimiters].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]], @@ -102,6 +104,7 @@ def self.configuration(directory = Dir.pwd) def self.check_configuration(configuration) raise Error, "#{CONFIG_FILE}: expected a mapping of settings" unless configuration.is_a?(Hash) + check_names(configuration.keys, SETTINGS, "a setting") %w[linters format fmt].each do |key| raise Error, "#{CONFIG_FILE}: `#{key}` must be a mapping" unless configuration.fetch(key, {}).is_a?(Hash) end @@ -118,7 +121,11 @@ def self.check_rule_options(rule_name, options) 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 = Rules.all.find { _1.rule_name == rule_name } + raise Error, "#{CONFIG_FILE}: `#{rule_name}` is not a rule of super-slim lint" unless rule_class + + preset_option = (PRESETS.any? { |_preset, options_by_rule| options_by_rule.key?(rule_name) }) ? ["preset"] : [] + check_names(options.keys, rule_class::DEFAULTS.keys.map(&:to_s) + GLOB_OPTIONS + preset_option, "an option of #{rule_name}") rule_class::DEFAULTS.each do |key, default| next unless options.key?(key.to_s) @@ -129,6 +136,7 @@ def self.check_rule_options(rule_name, options) private_class_method :check_rule_options def self.check_format_options(key, options) + check_names(options.keys, FORMAT_SETTINGS, "a setting of `#{key}`") 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 @@ -140,6 +148,14 @@ def self.check_format_options(key, options) end private_class_method :check_format_options + # A misspelt name is a setting that does nothing and says nothing: the rule it meant to turn off stays on. + def self.check_names(names, known_names, kind) + unknown_name = (names - known_names).first or return + + raise Error, "#{CONFIG_FILE}: `#{unknown_name}` is not #{kind}, use one of: #{known_names.join(", ")}" + end + private_class_method :check_names + # include: and exclude: globs match paths relative to `root`, the directory of .super-slim.yml. def initialize(options_by_rule_name = {}, root = Dir.pwd) @root = File.join(File.expand_path(root), "") diff --git a/test/cli_test.rb b/test/cli_test.rb index 75e2763..5bacacf 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -117,6 +117,22 @@ def test_a_configuration_of_the_wrong_shape_fails_with_a_usage_error end end + def test_a_name_the_configuration_misspells_fails_with_a_usage_error + { + "linters:\n LineLenght:\n max: 150\n" => "`LineLenght` is not a rule of super-slim lint", + "linters:\n LineLength:\n maximum: 150\n" => "`maximum` is not an option of LineLength, use one of: enabled, max, include, exclude", + "linters:\n LineLength:\n preset: bootstrap5\n" => "`preset` is not an option of LineLength, use one of: enabled, max, include, exclude", + "formatter:\n max_line_length: 120\n" => "`formatter` is not a setting, use one of: linters, format, fmt, tailwind_classes", + "format:\n line_length: 120\n" => "`line_length` is not a setting of `format`, use one of: max_line_length, attribute_delimiters" + }.each do |configuration, message| + in_templates("p Hello\n") do |directory| + File.write(File.join(directory, ".super-slim.yml"), configuration) + + assert_output("", ".super-slim.yml: #{message}\n") { assert_equal SuperSlim::CLI::EX_USAGE, SuperSlim::CLI.new.call(["lint", "."]) } + end + 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")