Module: ArchSpec::Formatters::Text
- Defined in:
- lib/archspec/formatters/text.rb
Overview
Prints diagnostics the way clang and herb do: a severity header with the rule id, the location, a code frame with the offending span underlined, and the evidence as a note.
[error] models must not depend on controllers [dependencies.forbid]
app/models/user.rb:3:3
2 │ class User
→ 3 │ UsersController
│ ^~~~~~~~~~~~~~~
4 │ end
note: User references UsersController
Output to a terminal is colored; a non-TTY or a NO_COLOR environment disables the colors.
Constant Summary collapse
- CONTEXT_LINES =
1
Class Method Summary collapse
-
.note_for(diagnostic, relative) ⇒ Object
The evidence as a note, or nil when it would only repeat the location shown above it, as parse-error evidence does.
- .print(output = $stdout, graph:, diagnostics:) ⇒ Object
- .print_census(output, graph) ⇒ Object
- .print_diagnostic(output, style, graph, diagnostic, sources) ⇒ Object
- .print_frame(output, style, location, lines) ⇒ Object
- .read_lines(path) ⇒ Object
- .underline(location, text) ⇒ Object
Class Method Details
.note_for(diagnostic, relative) ⇒ Object
The evidence as a note, or nil when it would only repeat the location shown above it, as parse-error evidence does.
87 88 89 90 91 92 93 |
# File 'lib/archspec/formatters/text.rb', line 87 def note_for(diagnostic, relative) note = diagnostic.evidence.to_s return if note.empty? || note == relative note = "#{note} (confidence: #{diagnostic.confidence})" unless diagnostic.confidence == :high note end |
.print(output = $stdout, graph:, diagnostics:) ⇒ Object
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/archspec/formatters/text.rb', line 27 def print(output = $stdout, graph:, diagnostics:) if diagnostics.empty? output.puts "ArchSpec passed: #{graph.files.size} files, #{graph.constants.size} constants, " \ "#{graph.edges.size} facts checked." print_census(output, graph) return end style = Style.new(output) sources = Hash.new { |hash, path| hash[path] = read_lines(path) } diagnostics.each do |diagnostic| print_diagnostic(output, style, graph, diagnostic, sources) end label = diagnostics.size == 1 ? 'architecture violation' : 'architecture violations' output.puts style.bold("#{diagnostics.size} #{label} found.") print_census(output, graph) end |
.print_census(output, graph) ⇒ Object
115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/archspec/formatters/text.rb', line 115 def print_census(output, graph) census = graph.analysis_census clauses = [] if census[:unresolved_constants].positive? clauses << "#{census[:unresolved_constants]} unresolved constant references" end clauses << "#{census[:dynamic_features]} dynamic features" if census[:dynamic_features].positive? clauses << "#{census[:unknown_receivers]} calls with unknown receivers" if census[:unknown_receivers].positive? census[:rubydex_diagnostics].each do |rule, count| clauses << "#{count} RubyDEX #{rule} #{count == 1 ? 'diagnostic' : 'diagnostics'}" end output.puts "Analysis gaps: #{clauses.join(', ')}." unless clauses.empty? end |
.print_diagnostic(output, style, graph, diagnostic, sources) ⇒ Object
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/archspec/formatters/text.rb', line 47 def print_diagnostic(output, style, graph, diagnostic, sources) location = diagnostic.location relative = location.relative_path(graph.root) header = "#{style.severity('[error]')} #{style.bold(diagnostic.)} #{style.faint("[#{diagnostic.rule}]")}" output.puts header output.puts output.puts "#{relative}:#{location.line}:#{location.column}" print_frame(output, style, location, sources[location.path]) if (note = note_for(diagnostic, relative)) output.puts output.puts " #{style.note('note:')} #{note}" end output.puts " #{style.note('reason:')} #{diagnostic.reason}" if diagnostic.reason output.puts end |
.print_frame(output, style, location, lines) ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/archspec/formatters/text.rb', line 65 def print_frame(output, style, location, lines) target = lines[location.line - 1] return unless target output.puts first = [location.line - CONTEXT_LINES, 1].max last = [location.line + CONTEXT_LINES, lines.size].min width = last.to_s.length (first..last).each do |number| text = lines[number - 1] if number == location.line output.puts " #{style.marker('→')} #{style.faint("#{number.to_s.rjust(width)} │")} #{text}" output.puts " #{style.faint("#{' ' * width} │")} #{style.marker(underline(location, target))}" else output.puts " #{style.faint("#{number.to_s.rjust(width)} │")} #{text}" end end end |
.read_lines(path) ⇒ Object
107 108 109 110 111 112 113 |
# File 'lib/archspec/formatters/text.rb', line 107 def read_lines(path) return [] unless File.file?(path) File.readlines(path, chomp: true).map { |line| line.scrub.tr("\t", ' ') } rescue SystemCallError [] end |
.underline(location, text) ⇒ Object
95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/archspec/formatters/text.rb', line 95 def underline(location, text) span = if location.end_line == location.line location.end_column - location.column else text.length - location.column + 1 end span = span.clamp(1, [text.length - location.column + 1, 1].max) "#{' ' * (location.column - 1)}^#{'~' * (span - 1)}" end |