Class: SasLinter::Rules::UnreachableInnerBranchValue

Inherits:
SasLinter::Rule show all
Defined in:
lib/sas_linter/rules/unreachable_inner_branch_value.rb

Overview

Flag inner branches whose comparison values are excluded by an enclosing ‘if VAR in (…) then do; … end;` guard.

Motivating shape: an outer guard ‘if RANK in (0,1,2,3,4,5,6,8) then do;` omits 7, while an inner `if RANK in (5,6,7,8) then cOut = 2;` lists 7. Value 7 falls through the outer guard, so the inner branch can never fire for it and cOut silently stays missing.

Detection: outer guard pushes a allowed_set frame; inner ‘if VAR in (…)`, `if VAR = N`, or `if VAR eq N` references inside the same DO block are checked against that set. Values absent from the outer set produce a finding.

Constant Summary collapse

TT =
SasLexer::Lexer::TokenType

Instance Attribute Summary

Attributes inherited from SasLinter::Rule

#autofix

Instance Method Summary collapse

Methods inherited from SasLinter::Rule

all, #autofix?, description, fetch, from_config, inherited, #initialize, register, registry, rule_id, severity, supports_autofix?

Constructor Details

This class inherits a constructor from SasLinter::Rule

Instance Method Details

#check(tokens, path:, all_tokens: nil, source: nil) ⇒ Object

Outer guard pattern: KW_IF IDENT KW_IN LPAREN <lits…> RPAREN KW_THEN KW_DO SEMI Inner check patterns:

KW_IF IDENT(V) KW_IN LPAREN <lits...> RPAREN
KW_IF IDENT(V) KW_EQ <lit>
KW_IF IDENT(V) ASSIGN <lit>     (SAS uses `=` as comparison in IF)


35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/sas_linter/rules/unreachable_inner_branch_value.rb', line 35

def check(tokens, path:, all_tokens: nil, source: nil) # rubocop:disable Lint/UnusedMethodArgument
  findings = []
  guard_stack = [] # array of {var:, allowed:, depth:}
  do_depth = 0
  i = 0

  while i < tokens.length
    tok = tokens[i]

    if tok[:type] == TT::KW_IF
      consumed, frame, inner_findings =
        analyze_if(tokens, i, do_depth, guard_stack, path)
      findings.concat(inner_findings)
      if frame
        guard_stack.push(frame)
        do_depth += 1
      end
      i += consumed
      next
    end

    if tok[:type] == TT::KW_DO
      # bare `do;` (no IF prefix), or `do i = 1 to N;` — both increment depth
      do_depth += 1
      i += 1
      next
    end

    if tok[:type] == TT::KW_END
      do_depth -= 1 if do_depth > 0
      guard_stack.pop while guard_stack.last && guard_stack.last[:depth] > do_depth
      i += 1
      next
    end

    i += 1
  end

  findings
end