From f95c530640de9fefed144040804acc9cf02ea46f Mon Sep 17 00:00:00 2001 From: Steven Pritchard Date: Wed, 11 Mar 2026 21:44:39 +0000 Subject: [PATCH] Make CLI more flexible * Add `.scelint` defaults file support (rspec-style CLI argument defaults) * Add `--allow-reserved-words` option to skip reserved word checks on parameter names * Bump version to 0.6.0 --- CHANGELOG.md | 4 + lib/scelint.rb | 5 +- lib/scelint/cli.rb | 16 +++- lib/scelint/version.rb | 2 +- .../SIMP/compliance_profiles/ces.yaml | 6 ++ .../SIMP/compliance_profiles/checks.yaml | 10 +++ .../SIMP/compliance_profiles/profile.yaml | 6 ++ spec/unit/scelint/cli_spec.rb | 84 +++++++++++++++++++ spec/unit/scelint/lint_spec.rb | 2 +- 9 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 spec/fixtures/modules/test_module_12/SIMP/compliance_profiles/ces.yaml create mode 100644 spec/fixtures/modules/test_module_12/SIMP/compliance_profiles/checks.yaml create mode 100644 spec/fixtures/modules/test_module_12/SIMP/compliance_profiles/profile.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 2315532..21e0b6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 0.6.0 / 2026-03-11 +* Add `.scelint` defaults file support (rspec-style CLI argument defaults) +* Add `--allow-reserved-words` option to skip reserved word checks on parameter names + ### 0.5.0 / 2026-03-11 * Validate parameter names * Fix command-line argument processing diff --git a/lib/scelint.rb b/lib/scelint.rb index e0d5ac0..d62c25d 100644 --- a/lib/scelint.rb +++ b/lib/scelint.rb @@ -134,8 +134,9 @@ class Lint # # @param paths [Array] Paths to look for SCE data in. Defaults to ['.'] # @param logger [Logger] A logger to send messages to. Defaults to an instance of Logger with the log level set to INFO. - def initialize(paths = ['.'], logger: Logger.new(STDOUT, level: Logger::INFO)) + def initialize(paths = ['.'], logger: Logger.new(STDOUT, level: Logger::INFO), allow_reserved_words: false) @log = logger + @allow_reserved_words = allow_reserved_words @errors = [] @warnings = [] @notes = [] @@ -418,6 +419,8 @@ def check_parameter(file, check, parameter) return end + return if @allow_reserved_words + parameter.split('::').each do |part| if reserved_words.include?(part) errors << "#{file} (check '#{check}'): parameter name '#{parameter}' contains reserved word '#{part}'" diff --git a/lib/scelint/cli.rb b/lib/scelint/cli.rb index e38db83..b07d916 100644 --- a/lib/scelint/cli.rb +++ b/lib/scelint/cli.rb @@ -10,10 +10,21 @@ def self.exit_on_failure? true end + DEFAULTS_FILE = '.scelint' + + # Load default arguments from .scelint in the current directory. + # Each non-blank, non-comment line may contain one or more arguments. + def self.load_defaults + return [] unless File.exist?(DEFAULTS_FILE) + File.readlines(DEFAULTS_FILE, chomp: true) + .reject { |line| line.strip.empty? || line.strip.start_with?('#') } + .flat_map(&:split) + end + # When the first argument is not a known subcommand or an option flag, # treat all arguments as paths for the default `lint` command. def self.start(given_args = ARGV, config = {}) - args = given_args + args = load_defaults + given_args if args.first && !args.first.start_with?('-') && !all_commands.key?(args.first) args = ['lint'] + args end @@ -26,9 +37,10 @@ def self.start(given_args = ARGV, config = {}) desc 'lint PATH', 'Lint all files in PATH' option :strict, type: :boolean, aliases: '-s', default: false + option :allow_reserved_words, type: :boolean, default: false def lint(*paths) paths = ['.'] if paths.nil? || paths.empty? - lint = Scelint::Lint.new(paths, logger: logger) + lint = Scelint::Lint.new(paths, logger: logger, allow_reserved_words: options[:allow_reserved_words]) count = lint.files.count diff --git a/lib/scelint/version.rb b/lib/scelint/version.rb index 62f0901..97e8c68 100644 --- a/lib/scelint/version.rb +++ b/lib/scelint/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Scelint - VERSION = '0.5.0' + VERSION = '0.6.0' end diff --git a/spec/fixtures/modules/test_module_12/SIMP/compliance_profiles/ces.yaml b/spec/fixtures/modules/test_module_12/SIMP/compliance_profiles/ces.yaml new file mode 100644 index 0000000..b722585 --- /dev/null +++ b/spec/fixtures/modules/test_module_12/SIMP/compliance_profiles/ces.yaml @@ -0,0 +1,6 @@ +--- +version: 2.0.0 +ce: + 12_ce1: + controls: + 12_control1: true diff --git a/spec/fixtures/modules/test_module_12/SIMP/compliance_profiles/checks.yaml b/spec/fixtures/modules/test_module_12/SIMP/compliance_profiles/checks.yaml new file mode 100644 index 0000000..a93738a --- /dev/null +++ b/spec/fixtures/modules/test_module_12/SIMP/compliance_profiles/checks.yaml @@ -0,0 +1,10 @@ +--- +version: 2.0.0 +checks: + 12_settings_check: + type: puppet-class-parameter + settings: + parameter: test_module_12::settings + value: true + ces: + - 12_ce1 diff --git a/spec/fixtures/modules/test_module_12/SIMP/compliance_profiles/profile.yaml b/spec/fixtures/modules/test_module_12/SIMP/compliance_profiles/profile.yaml new file mode 100644 index 0000000..3ef321e --- /dev/null +++ b/spec/fixtures/modules/test_module_12/SIMP/compliance_profiles/profile.yaml @@ -0,0 +1,6 @@ +--- +version: 2.0.0 +profiles: + 12_profile_test: + controls: + 12_control1: true diff --git a/spec/unit/scelint/cli_spec.rb b/spec/unit/scelint/cli_spec.rb index 6ed41d8..f7e0081 100644 --- a/spec/unit/scelint/cli_spec.rb +++ b/spec/unit/scelint/cli_spec.rb @@ -7,6 +7,7 @@ let(:fixtures_dir) { File.expand_path('../../fixtures/modules', __dir__) } let(:clean_module_path) { File.join(fixtures_dir, 'test_module_01') } let(:warning_module_path) { File.join(fixtures_dir, 'test_module_04') } + let(:reserved_word_module_path) { File.join(fixtures_dir, 'test_module_12') } # Runs the CLI via .start (as a user would invoke from the command line), # captures the SystemExit status, and suppresses stdout noise from the logger. @@ -19,6 +20,8 @@ def run_cli(args) before(:each) do # Silence logger output to keep spec output clean. allow(Logger).to receive(:new).and_return(Logger.new(File::NULL)) + # Prevent any real .scelint file in the working directory from affecting tests. + allow(described_class).to receive(:load_defaults).and_return([]) end describe 'invocation without an explicit subcommand' do @@ -85,4 +88,85 @@ def run_cli(args) expect(run_cli(['--debug', clean_module_path])).to eq(0) end end + + describe '.scelint defaults file' do + describe '.load_defaults' do + before(:each) do + allow(described_class).to receive(:load_defaults).and_call_original + end + + it 'returns an empty array when no .scelint file exists' do + allow(File).to receive(:exist?).with(described_class::DEFAULTS_FILE).and_return(false) + expect(described_class.load_defaults).to eq([]) + end + + it 'returns args parsed from the file' do + allow(File).to receive(:exist?).with(described_class::DEFAULTS_FILE).and_return(true) + allow(File).to receive(:readlines).with(described_class::DEFAULTS_FILE, chomp: true) + .and_return(['--allow-reserved-words', '--strict']) + expect(described_class.load_defaults).to eq(['--allow-reserved-words', '--strict']) + end + + it 'splits multiple args on a single line' do + allow(File).to receive(:exist?).with(described_class::DEFAULTS_FILE).and_return(true) + allow(File).to receive(:readlines).with(described_class::DEFAULTS_FILE, chomp: true) + .and_return(['--allow-reserved-words --strict']) + expect(described_class.load_defaults).to eq(['--allow-reserved-words', '--strict']) + end + + it 'ignores blank lines' do + allow(File).to receive(:exist?).with(described_class::DEFAULTS_FILE).and_return(true) + allow(File).to receive(:readlines).with(described_class::DEFAULTS_FILE, chomp: true) + .and_return(['', '--strict', '']) + expect(described_class.load_defaults).to eq(['--strict']) + end + + it 'ignores comment lines' do + allow(File).to receive(:exist?).with(described_class::DEFAULTS_FILE).and_return(true) + allow(File).to receive(:readlines).with(described_class::DEFAULTS_FILE, chomp: true) + .and_return(['# this is a comment', '--strict']) + expect(described_class.load_defaults).to eq(['--strict']) + end + end + + context 'when .scelint contains --allow-reserved-words' do + before(:each) do + allow(described_class).to receive(:load_defaults).and_return(['--allow-reserved-words']) + end + + it 'applies the flag without passing it on the command line' do + expect(run_cli([reserved_word_module_path])).to eq(0) + end + + it 'can be overridden by --no-allow-reserved-words on the command line' do + expect(run_cli(['--no-allow-reserved-words', reserved_word_module_path])).to eq(1) + end + end + + context 'when .scelint contains --strict' do + before(:each) do + allow(described_class).to receive(:load_defaults).and_return(['--strict']) + end + + it 'applies --strict without passing it on the command line' do + expect(run_cli([warning_module_path])).to eq(1) + end + end + end + + describe '--allow-reserved-words flag' do + context 'when a parameter name contains a reserved word' do + it 'exits 1 without --allow-reserved-words' do + expect(run_cli([reserved_word_module_path])).to eq(1) + end + + it 'exits 0 with --allow-reserved-words' do + expect(run_cli(['--allow-reserved-words', reserved_word_module_path])).to eq(0) + end + + it 'exits 0 with --allow-reserved-words and --strict' do + expect(run_cli(['--allow-reserved-words', '--strict', reserved_word_module_path])).to eq(0) + end + end + end end diff --git a/spec/unit/scelint/lint_spec.rb b/spec/unit/scelint/lint_spec.rb index 39ac649..d05c757 100644 --- a/spec/unit/scelint/lint_spec.rb +++ b/spec/unit/scelint/lint_spec.rb @@ -6,7 +6,7 @@ # Each test assumes 3 files, no errors, no warnings, no notes. # Exceptions are listed below. let(:lint_files) { { '04' => 37, '11' => 2 } } - let(:lint_errors) { {} } + let(:lint_errors) { { '12' => 2 } } let(:lint_warnings) { { '04' => 17 } } let(:lint_notes) { { '11' => 1 } }