Class: SasLinter
- Inherits:
-
Object
- Object
- SasLinter
- Defined in:
- lib/sas_linter.rb,
lib/sas_linter/version.rb,
lib/sas_linter/formatter.rb,
lib/sas_linter/rules/line_endings.rb,
lib/sas_linter/rules/tab_expansion.rb,
lib/sas_linter/rules/source_headers.rb,
lib/sas_linter/rules/encoding_issues.rb,
lib/sas_linter/rules/choose_one_template.rb,
lib/sas_linter/rules/commented_out_guard.rb,
lib/sas_linter/rules/trailing_whitespace.rb,
lib/sas_linter/rules/unterminated_comment.rb,
lib/sas_linter/rules/malformed_if_condition.rb,
lib/sas_linter/rules/invalid_numeric_literal.rb,
lib/sas_linter/rules/malformed_label_statement.rb,
lib/sas_linter/rules/identical_if_else_branches.rb,
lib/sas_linter/rules/inconsistent_variable_case.rb,
lib/sas_linter/rules/format_for_unknown_variable.rb,
lib/sas_linter/rules/missing_assignment_semicolon.rb,
lib/sas_linter/rules/unreachable_inner_branch_value.rb,
lib/sas_linter/rules/variable_value_out_of_known_range.rb
Overview
Configurable lint engine for SAS source files. Walks the token stream
produced by SasLexer::Lexer and applies a set of pluggable rules.
Each rule is a subclass of SasLinter::Rule and is auto-registered when
its file is required. Use SasLinter.new(rules: [...]) to constrain the
rule set, or SasLinter.from_config(config_hash) to honor a YAML config.
Defined Under Namespace
Modules: Rules Classes: Finding, Formatter, Rule
Constant Summary collapse
- DEFAULT_CONFIG_PATH =
"config/lint.yaml"- VERSION =
"0.2.6"
Class Method Summary collapse
-
.from_config(config) ⇒ Object
Build a linter from a parsed config hash.
-
.load_config_file(path) ⇒ Object
Load a YAML config file and return a parsed hash.
Instance Method Summary collapse
-
#format_file(path, formatter:) ⇒ Object
Apply formatting to a file in-place.
-
#initialize(rules: nil) ⇒ SasLinter
constructor
A new instance of SasLinter.
-
#lint(source, path: "(string)") ⇒ Object
Lint a SAS source string.
-
#lint_file(path) ⇒ Object
Lint a file by path.
-
#lint_with_fixes(source, path: "(string)") ⇒ Object
Lint a SAS source string and apply any autofixes from rules whose
autofix?instance flag is true.
Constructor Details
#initialize(rules: nil) ⇒ SasLinter
Returns a new instance of SasLinter.
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/sas_linter.rb', line 140 def initialize(rules: nil) classes_or_instances = if rules.nil? Rule.all else rules end @rules = classes_or_instances.map do |r| case r when Rule then r when Class then r.new else Rule.fetch(r).new end end end |
Class Method Details
.from_config(config) ⇒ Object
Build a linter from a parsed config hash. Schema:
rules:
<rule_id>:
enabled: true|false # default: true
<option>: <value> # passed to Rule.from_config
Rules omitted from the config default to enabled with no options, so
adding a new rule to the gem won't silently disable it for users with
an existing config file. To suppress a rule, list it with enabled: false.
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/sas_linter.rb', line 167 def self.from_config(config) config = (config || {}).transform_keys(&:to_s) rules_config = (config["rules"] || {}).transform_keys(&:to_s) instances = [] rules_config.each do |id, opts| opts = (opts || {}).transform_keys(&:to_s) next if opts["enabled"] == false klass = Rule.fetch(id.to_sym) instances << klass.from_config(opts.reject { |k, _| k == "enabled" }) end Rule.all.each do |klass| next if rules_config.key?(klass.rule_id.to_s) instances << klass.new end new(rules: instances) end |
.load_config_file(path) ⇒ Object
Load a YAML config file and return a parsed hash. Returns an empty hash
when the file is missing — the default config/lint.yaml is optional.
191 192 193 194 195 |
# File 'lib/sas_linter.rb', line 191 def self.load_config_file(path) return {} unless File.file?(path) YAML.safe_load_file(path) || {} end |
Instance Method Details
#format_file(path, formatter:) ⇒ Object
Apply formatting to a file in-place. Runs the formatter's own
transformations first, then the autofix pipeline for rules that have
autofix: true in config — identical to lint_with_fixes except that the
formatter pass runs first. Rules that haven't been opted in to autofix
(e.g. missing_assignment_semicolon without explicit autofix: true) are
left alone so that --format stays cosmetic by default.
Returns true if the file was rewritten, false if nothing changed.
252 253 254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/sas_linter.rb', line 252 def format_file(path, formatter:) original = read_source(path) modified = formatter.format(original) @rules.each do |rule| next unless rule.autofix? && rule.class.supports_autofix? modified = rule.autofix(modified) end return false if modified.b == original.b File.write(path, modified) true end |
#lint(source, path: "(string)") ⇒ Object
Lint a SAS source string. path is used for finding location output.
Returns just the findings array. Use lint_with_fixes when the caller
wants the (possibly-modified) source back too.
200 201 202 |
# File 'lib/sas_linter.rb', line 200 def lint(source, path: "(string)") lint_with_fixes(source, path: path).first end |
#lint_file(path) ⇒ Object
Lint a file by path. Sources are commonly Windows-1252 or ISO-8859-1 rather than UTF-8 — read as binary and best-effort transcode so the lexer (which requires valid UTF-8) doesn't reject them.
If any autofix-enabled rule rewrote the source, the file is updated in place. Returns the findings array regardless of write outcome.
The modified.b != original.b guard compares raw bytes so a
difference in encoding tags alone (e.g. UTF-8 vs ASCII-8BIT)
doesn't trigger a write. That can happen when EncodingIssues
autofix returns a binary string but no rule actually changed any
bytes — without .b the file would be rewritten with byte-
identical contents and a different encoding label, surfacing as
a no-op diff in git that overwrites the user's chosen encoding.
237 238 239 240 241 242 |
# File 'lib/sas_linter.rb', line 237 def lint_file(path) original = read_source(path) findings, modified = lint_with_fixes(original, path: path) File.write(path, modified) if modified.b != original.b findings end |
#lint_with_fixes(source, path: "(string)") ⇒ Object
Lint a SAS source string and apply any autofixes from rules whose
autofix? instance flag is true. Returns [findings, modified_source].
When no rule has autofix enabled the modified source equals the input.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/sas_linter.rb', line 207 def lint_with_fixes(source, path: "(string)") default_tokens, all_tokens = tokenize(source) findings = @rules.flat_map do |rule| rule.check(default_tokens, path: path, all_tokens: all_tokens, source: source) end modified = source @rules.each do |rule| next unless rule.autofix? && rule.class.supports_autofix? modified = rule.autofix(modified) end [findings, modified] end |