Module: Sloplint::Engine
- Defined in:
- lib/sloplint/engine.rb
Constant Summary collapse
- CONTEXT_CHARS =
40
Class Method Summary collapse
-
.blank_markdown(text) ⇒ Object
Replace fenced code, inline code, and URLs with same-length whitespace so line/column stay correct.
-
.context_for(source, match) ⇒ Object
The match plus ~CONTEXT_CHARS either side, bracketed, whitespace collapsed.
-
.line_col(text, offset, line_starts: nil) ⇒ Object
1-indexed line and column for a char offset into text.
-
.line_starts_for(text) ⇒ Object
Char offset where each line begins, index 0 = line 1.
-
.scan(text, rules: RULES, markdown: false, path: "-") ⇒ Object
text: the source.
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.
100 101 102 103 104 105 106 |
# File 'lib/sloplint/engine.rb', line 100 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 |
.context_for(source, match) ⇒ Object
The match plus ~CONTEXT_CHARS either side, bracketed, whitespace collapsed.
A bare excerpt is useless for a rule whose match is one character -- see
em-dash, where the note said only "—" and you had to open the file and
count to the column to learn anything.
Truncated ends get an ellipsis and are trimmed back to a word boundary so the window doesn't open mid-word. A match already CONTEXT_CHARS long carries its own context; padding it just makes an em-dash-overuse span (which can run a whole paragraph) longer for no gain, so those return the match alone.
source is the PRE-blanking text. blank_markdown replaces each non-newline char with one space, so offsets are identical either way, but a window over blanked text shows code and URLs as a run of spaces. Offsets here are character offsets (MatchData#begin), matching the char-based line_starts_for.
60 61 62 63 64 65 66 67 68 69 |
# File 'lib/sloplint/engine.rb', line 60 def context_for(source, match) return "[#{match[0].gsub(/\s+/, " ").strip}]" if match[0].length >= CONTEXT_CHARS b, e = match.begin(0), match.end(0) pre = source[[b - CONTEXT_CHARS, 0].max...b] post = source[e, CONTEXT_CHARS].to_s pre = "…#{pre.sub(/\A\S*\s+/, "")}" if b > CONTEXT_CHARS post = "#{post.sub(/\s+\S*\z/, "")}…" if e + CONTEXT_CHARS < source.length "#{pre}[#{match[0]}]#{post}".gsub(/\s+/, " ").strip 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.
77 78 79 80 81 |
# File 'lib/sloplint/engine.rb', line 77 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.
91 92 93 94 95 96 |
# File 'lib/sloplint/engine.rb', line 91 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).
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/sloplint/engine.rb', line 19 def scan(text, rules: RULES, markdown: false, path: "-") source = text 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:) = count ? rule. % { count: count } : rule. notes << Note.new( path: path, line: line, column: column, severity: rule.severity, rule: rule.id, category: rule.category, message: , excerpt: matched.gsub(/\s+/, " ").strip, context: context_for(source, m), count: count, rationale: rule.rationale, suggestion: rule.suggestion ) end end notes.sort_by { |n| [n.line, n.column] } end |