Class: SasLinter::Rules::VariableValueOutOfKnownRange

Inherits:
SasLinter::Rule show all
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

#autofix

Class Method Summary collapse

Instance Method Summary collapse

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.expand_path(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