Class: SasLinter::Rules::VariableValueOutOfKnownRange
- Inherits:
-
SasLinter::Rule
- Object
- SasLinter::Rule
- SasLinter::Rules::VariableValueOutOfKnownRange
- Defined in:
- lib/sas_linter/rules/variable_value_out_of_known_range.rb
Overview
Flag conditional comparisons against literal values that fall outside a variable’s documented acceptable values, e.g.:
if AGE in (0,1,2,99) then ... # AGE takes 0..2 — `99` is dead
if SCORE = 99 then ... # SCORE takes 0..5
if RANK eq 7 then ... # RANK takes 0..6
Catches typos and stale literals where the source compares a variable against a value the variable can never actually take, so the branch is unreachable. Only fires inside ‘if`-conditions (between KW_IF and the next KW_THEN or SEMI) — assignments to the variable are not flagged.
Acceptable values are loaded from one or more CSV files with two configurable columns: a name column and an acceptable-values column. Recognized value formats:
0-5 → integer range
1,2,3 → integer set
0-4,7,8 → range plus extras
0-90 (99) → range plus parenthesized extras
Variables whose values column is free text or a date pattern are silently skipped (no findings, no errors).
Recognized config options:
csv_paths: ["metadata/variables.csv", ...] # required, at least one
name_column: "Variable" # default: "Variable"
values_column: "Acceptable Values" # default: "Acceptable Values"
name_match: case_insensitive | exact # default: case_insensitive
autofix: false # this rule has no autofix
When ‘csv_paths` is empty the rule is a no-op — useful so projects without a variable catalog can keep the rule registered without it firing.
Constant Summary collapse
- TT =
SasLexer::Lexer::TokenType
- DEFAULT_NAME_COLUMN =
"Variable"- DEFAULT_VALUES_COLUMN =
"Acceptable Values"- DEFAULT_DELIMITER =
","
Instance Attribute Summary
Attributes inherited from SasLinter::Rule
Class Method Summary collapse
Instance Method Summary collapse
-
#check(tokens, path:, all_tokens: nil, source: nil) ⇒ Object
rubocop:disable Lint/UnusedMethodArgument.
-
#initialize(csv_paths: [], name_column: DEFAULT_NAME_COLUMN, values_column: DEFAULT_VALUES_COLUMN, name_match: :case_insensitive, delimiter: DEFAULT_DELIMITER, autofix: false) ⇒ VariableValueOutOfKnownRange
constructor
A new instance of VariableValueOutOfKnownRange.
Methods inherited from SasLinter::Rule
all, #autofix?, description, fetch, inherited, register, registry, rule_id, severity, supports_autofix?
Constructor Details
#initialize(csv_paths: [], name_column: DEFAULT_NAME_COLUMN, values_column: DEFAULT_VALUES_COLUMN, name_match: :case_insensitive, delimiter: DEFAULT_DELIMITER, autofix: false) ⇒ VariableValueOutOfKnownRange
Returns a new instance of VariableValueOutOfKnownRange.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/sas_linter/rules/variable_value_out_of_known_range.rb', line 55 def initialize(csv_paths: [], name_column: DEFAULT_NAME_COLUMN, values_column: DEFAULT_VALUES_COLUMN, name_match: :case_insensitive, delimiter: DEFAULT_DELIMITER, autofix: false) super(autofix: autofix) @csv_paths = Array(csv_paths) @name_column = name_column @values_column = values_column @delimiter = delimiter unless %i[case_insensitive exact].include?(name_match) raise ArgumentError, "name_match must be :case_insensitive or :exact (got #{name_match.inspect})" end @name_match = name_match @specs = nil end |
Class Method Details
.from_config(opts = {}) ⇒ Object
74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/sas_linter/rules/variable_value_out_of_known_range.rb', line 74 def self.from_config(opts = {}) opts = opts.transform_keys(&:to_s) new( csv_paths: Array(opts["csv_paths"]).map { |p| File.(p) }, name_column: opts["name_column"] || DEFAULT_NAME_COLUMN, values_column: opts["values_column"] || DEFAULT_VALUES_COLUMN, name_match: (opts["name_match"] || "case_insensitive").to_sym, delimiter: opts["delimiter"] || DEFAULT_DELIMITER, autofix: opts["autofix"] ? true : false ) end |
Instance Method Details
#check(tokens, path:, all_tokens: nil, source: nil) ⇒ Object
rubocop:disable Lint/UnusedMethodArgument
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/sas_linter/rules/variable_value_out_of_known_range.rb', line 86 def check(tokens, path:, all_tokens: nil, source: nil) # rubocop:disable Lint/UnusedMethodArgument return [] if specs.empty? findings = [] in_condition = false i = 0 while i < tokens.length tok = tokens[i] case tok[:type] when TT::KW_IF in_condition = true i += 1 next when TT::KW_THEN, TT::SEMI in_condition = false i += 1 next end if in_condition && tok[:type] == TT::IDENTIFIER op = tokens[i + 1] if op consumed, ident_findings = check_comparison(tokens, i, tok, op, path) findings.concat(ident_findings) if consumed > 0 i += consumed next end end end i += 1 end findings end |