Module: Sloplint::Engine

Defined in:
lib/sloplint/engine.rb

Class Method Summary collapse

Class Method Details

.blank_markdown(text) ⇒ Object

Replace fenced code, inline code, and URLs with same-length whitespace so line/column stay correct. Newlines are preserved.



70
71
72
73
74
75
76
# File 'lib/sloplint/engine.rb', line 70

def blank_markdown(text)
  blank = lambda { |s| s.gsub(/[^\n]/, " ") }
  text
    .gsub(/```.*?```/m) { |s| blank.call(s) }   # fenced code
    .gsub(/`[^`\n]*`/) { |s| blank.call(s) }    # inline code
    .gsub(%r{https?://\S+}) { |s| blank.call(s) } # bare URLs
end

.line_col(text, offset, line_starts: nil) ⇒ Object

1-indexed line and column for a char offset into text. Binary-searches a precomputed line_starts table (see line_starts_for) so a scan with many notes doesn't re-walk the prefix from offset 0 for every single one -- the previous version did text[0, offset] per note, which is O(n) per call and O(n * notes) overall, quadratic on a large file with many hits. line_starts is optional so this stays callable standalone.



47
48
49
50
51
# File 'lib/sloplint/engine.rb', line 47

def line_col(text, offset, line_starts: nil)
  line_starts ||= line_starts_for(text)
  idx = (line_starts.bsearch_index { |s| s > offset } || line_starts.length) - 1
  [idx + 1, offset - line_starts[idx] + 1]
end

.line_starts_for(text) ⇒ Object

Char offset where each line begins, index 0 = line 1. Computed once per scan and shared across every note instead of recomputed per note.

Built via each_line + line.length, not repeated String#index(pat, pos) calls -- on non-ASCII-only text (e.g. curly quotes, em dashes), index with a start position is not O(1)-amortized per call in CRuby, and a few thousand newlines turned this quadratic: 7.4s on a 1.2MB UTF-8 file that each_line does in 0.004s.



61
62
63
64
65
66
# File 'lib/sloplint/engine.rb', line 61

def line_starts_for(text)
  starts = [0]
  text.each_line { |line| starts << starts.last + line.length }
  starts.pop unless text.empty? || text.end_with?("\n")
  starts
end

.scan(text, rules: RULES, markdown: false, path: "-") ⇒ Object

text: the source. rules: which Rule objects to run. markdown: blank code/URLs first. path: label carried into each Note (e.g. filename or "-" for stdin).



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/sloplint/engine.rb', line 17

def scan(text, rules: RULES, markdown: false, path: "-")
  text = blank_markdown(text) if markdown
  line_starts = line_starts_for(text)
  notes = []
  rules.each do |rule|
    text.to_enum(:scan, rule.pattern).each do
      m = Regexp.last_match
      matched = m[0]
      next if rule.skip.any? { |re| matched.match?(re) }

      count = rule.count_group ? matched.scan(rule.count_group).size : nil
      line, column = line_col(text, m.begin(0), line_starts:)
      message = count ? rule.message % { count: count } : rule.message
      notes << Note.new(
        path: path, line: line, column: column,
        severity: rule.severity, rule: rule.id, category: rule.category,
        message: message, excerpt: matched.gsub(/\s+/, " ").strip,
        count: count, suggestion: rule.suggestion
      )
    end
  end
  notes.sort_by { |n| [n.line, n.column] }
end