Class: SasLinter
- Inherits:
-
Object
- Object
- SasLinter
- Defined in:
- lib/sas_linter.rb,
lib/sas_linter/version.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/malformed_if_condition.rb,
lib/sas_linter/rules/identical_if_else_branches.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, Rule
Constant Summary collapse
- DEFAULT_CONFIG_PATH =
"config/lint.yaml"- VERSION =
"0.1.0"
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
-
#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
#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 |