Class: SasLinter::Rules::MalformedIfCondition

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

Overview

Validate that ‘if … then` conditions form well-shaped boolean expressions. Catches authoring mistakes the lexer cheerfully accepts but that won’t run, e.g.

if A1 = 1 A2 = 2 then ...    * missing `and`/`or`
if A1 = 1 and then ...       * trailing operator
if = 1 then ...               * leading operator, no left operand
if then ...                   * empty condition
A1 = 1 then ...              * missing `if`
if (a = 1 and b = 2 then ...  * unbalanced parens

Strategy: at each ‘KW_IF`, walk forward to the matching top-level `KW_THEN` (or `;` for a subsetting `if`) running a tiny operand/operator state machine. Top-level only — anything inside parens is treated as a single sub-expression so function calls and `in (…)` lists don’t trigger false positives.

An orphan ‘KW_THEN` (one not consumed by an enclosing `if`) is reported as a likely missing `if`.

Constant Summary collapse

TT =
SasLexer::Lexer::TokenType
COMPARISON_OPS =
[
  TT::ASSIGN, TT::KW_EQ, TT::KW_NE, TT::NE, TT::KW_LT, TT::LT, TT::KW_LE, TT::LE,
  TT::KW_GT, TT::GT, TT::KW_GE, TT::GE, TT::KW_IN, TT::SOUNDS_LIKE, TT::GTLT, TT::LTGT,
  TT::KW_EQT, TT::KW_GTT, TT::KW_LTT, TT::KW_GET, TT::KW_LET, TT::KW_NET
].freeze
LOGICAL_OPS =
[TT::KW_AND, TT::KW_OR, TT::AMP, TT::PIPE, TT::PIPE2].freeze
ARITHMETIC_OPS =
[TT::PLUS, TT::MINUS, TT::STAR, TT::FSLASH, TT::STAR2,
TT::EXCL, TT::EXCL2, TT::BPIPE, TT::BPIPE2].freeze
BINOPS =
(COMPARISON_OPS + LOGICAL_OPS + ARITHMETIC_OPS).to_set.freeze
UNARY_PREFIXES =

‘+`/`-` are also binary; the state machine disambiguates by checking whether we currently expect an operand.

[TT::KW_NOT, TT::NOT, TT::MINUS, TT::PLUS].to_set.freeze
OPERAND_TOKENS =
[
  TT::IDENTIFIER,
  TT::INTEGER_LITERAL, TT::FLOAT_LITERAL, TT::FLOAT_EXPONENT_LITERAL,
  TT::STRING_LITERAL, TT::HEX_STRING_LITERAL, TT::BIT_TESTING_LITERAL,
  TT::DATE_LITERAL, TT::DATE_TIME_LITERAL, TT::TIME_LITERAL, TT::NAME_LITERAL,
  TT::MACRO_VAR_RESOLVE, TT::MACRO_IDENTIFIER, TT::MACRO_STRING,
  TT::STRING_EXPR_START
].to_set.freeze

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

rubocop:disable Lint/UnusedMethodArgument



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/sas_linter/rules/malformed_if_condition.rb', line 62

def check(tokens, path:, all_tokens: nil, source: nil) # rubocop:disable Lint/UnusedMethodArgument
  findings = []
  consumed_thens = {}
  i = 0

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

    if tok[:type] == TT::KW_IF
      new_i, sub_findings = analyze_if(tokens, i, path, consumed_thens)
      findings.concat(sub_findings)
      i = new_i
      next
    end

    if tok[:type] == TT::KW_THEN && !consumed_thens[i]
      findings << finding(
        line: tok[:start_line],
        column: tok[:start_column] + 1,
        message: "`then` without a preceding `if` condition — likely missing `if`.",
        path: path
      )
    end

    i += 1
  end

  findings
end