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

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.



83
84
85
86
87
88
89
# File 'lib/archspec/formatters/text.rb', line 83

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


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 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."
    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.")
end


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/archspec/formatters/text.rb', line 45

def print_diagnostic(output, style, graph, diagnostic, sources)
  location = diagnostic.location
  relative = location.relative_path(graph.root)

  output.puts "#{style.severity('[error]')} #{style.bold(diagnostic.message)} #{style.faint("[#{diagnostic.rule}]")}"
  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
end


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/archspec/formatters/text.rb', line 61

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



103
104
105
106
107
108
109
# File 'lib/archspec/formatters/text.rb', line 103

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



91
92
93
94
95
96
97
98
99
100
101
# File 'lib/archspec/formatters/text.rb', line 91

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