Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Steepfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ target :lib do
signature "sig"

check "lib/lrama/counterexamples"
check "lib/lrama/diagnostics"
check "lib/lrama/grammar"
check "lib/lrama/lexer"
check "lib/lrama/reporter"
Expand Down
4 changes: 4 additions & 0 deletions lib/lrama.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
require_relative "lrama/bitmap"
require_relative "lrama/command"
require_relative "lrama/context"
require_relative "lrama/diagnostics/color"
require_relative "lrama/diagnostics/message"
require_relative "lrama/diagnostics/formatter"
require_relative "lrama/diagnostics/reporter"
require_relative "lrama/counterexamples"
require_relative "lrama/diagram"
require_relative "lrama/digraph"
Expand Down
1 change: 1 addition & 0 deletions lib/lrama/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Command
def initialize(argv)
@logger = Lrama::Logger.new
@options = OptionParser.parse(argv)
Diagnostics::Color.setup(@options.color, $stderr)
@tracer = Tracer.new(STDERR, **@options.trace_opts)
@reporter = Reporter.new(**@options.report_opts)
@warnings = Warnings.new(@logger, @options.warnings)
Expand Down
158 changes: 158 additions & 0 deletions lib/lrama/diagnostics/color.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# rbs_inline: enabled
# frozen_string_literal: true

module Lrama
module Diagnostics
module Color
CODES = {
reset: "\e[0m",
bold: "\e[1m",
faint: "\e[2m",
italic: "\e[3m",
underline: "\e[4m",
blink: "\e[5m",
inverse: "\e[7m",
strikethrough: "\e[9m",

black: "\e[30m",
red: "\e[31m",
green: "\e[32m",
yellow: "\e[33m",
blue: "\e[34m",
magenta: "\e[35m",
cyan: "\e[36m",
white: "\e[37m",

bright_black: "\e[90m",
bright_red: "\e[91m",
bright_green: "\e[92m",
bright_yellow: "\e[93m",
bright_blue: "\e[94m",
bright_magenta: "\e[95m",
bright_cyan: "\e[96m",
bright_white: "\e[97m",

bg_black: "\e[40m",
bg_red: "\e[41m",
bg_green: "\e[42m",
bg_yellow: "\e[43m",
bg_blue: "\e[44m",
bg_magenta: "\e[45m",
bg_cyan: "\e[46m",
bg_white: "\e[47m"
}.freeze

SEMANTIC_STYLES = {
error: [:bold, :red],
warning: [:bold, :magenta],
note: [:bold, :cyan],
location: [:bold, :white],
caret: [:green],
range: [:green],
quote: [:yellow],
expected: [:green],
unexpected: [:red],
fixit_insert: [:green],
fixit_delete: [:strikethrough, :red],
trace: [:bright_black],
rule: [:cyan],
symbol: [:yellow]
}.freeze

class << self
# @rbs () -> bool
def enabled
@enabled ||= false
end

# @rbs (bool) -> bool
def enabled=(value)
@enabled = value
end

# @rbs (untyped text, *Symbol styles) -> String
def colorize(text, *styles)
return text.to_s unless @enabled
return text.to_s if styles.empty?

codes = resolve_styles(styles)
return text.to_s if codes.empty?

"#{codes.join}#{text}#{CODES[:reset]}"
end

# @rbs (untyped text) -> String
def strip(text)
text.to_s.gsub(/\e\[[0-9;]*m/, '')
end

# @rbs (?IO io) -> bool
def tty?(io = $stderr)
io.respond_to?(:tty?) && io.tty?
end

# @rbs (Symbol mode, ?IO io) -> bool
def should_colorize?(mode, io = $stderr)
return false if ENV.key?('NO_COLOR')

case mode
when :always then true
when :never then false
when :auto then tty?(io) && supports_color?
else false
end
end

# @rbs (Symbol mode, ?IO io) -> bool
def setup(mode, io = $stderr)
@enabled = should_colorize?(mode, io)
end

# @rbs () -> Symbol
def default_mode
case ENV['LRAMA_COLOR']&.downcase
when 'always', 'yes' then :always
when 'never', 'no' then :never
else :auto
end
end

# @rbs (String text, Symbol type) -> String
def for_diagnostic(text, type)
colorize(text, type)
end

private

# @rbs (Array[Symbol] styles) -> Array[String]
def resolve_styles(styles)
styles.flat_map { |style|
if SEMANTIC_STYLES.key?(style)
SEMANTIC_STYLES[style].map { |s| CODES[s] }
elsif CODES.key?(style)
[CODES[style]]
else
[]
end
}.compact
end

# @rbs () -> bool
def supports_color?
term = ENV['TERM']
return false if term.nil? || term.empty? || term == 'dumb'

term.include?('color') ||
term.include?('256') ||
term.include?('xterm') ||
term.include?('screen') ||
term.include?('vt100') ||
term.include?('ansi') ||
term.include?('linux') ||
term.include?('cygwin') ||
term.include?('rxvt')
end
end
end
end
end
182 changes: 182 additions & 0 deletions lib/lrama/diagnostics/formatter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# rbs_inline: enabled
# frozen_string_literal: true

module Lrama
module Diagnostics
class Formatter
GUTTER_WIDTH = 5
GUTTER_SEPARATOR = ' | '

# @rbs (?color_enabled: bool, ?show_source: bool, ?show_caret: bool) -> void
def initialize(color_enabled: false, show_source: true, show_caret: true)
@color_enabled = color_enabled
@show_source = show_source
@show_caret = show_caret
end

# @rbs (Message message) -> String
def format(message)
lines = [] #: Array[String]

lines << format_main_line(message)

if @show_source && message.source_line?
lines << format_source_line(message)

if @show_caret
lines << format_caret_line(message)
end

if message.fixit?
lines << format_fixit_line(message)
end
end

message.notes.each do |note|
lines << format_note(note)
end

lines.join("\n")
end

# @rbs (Array[Message] messages) -> String
def format_all(messages)
messages.map { |m| format(m) }.join("\n\n")
end

private

# @rbs (Message message) -> String
def format_main_line(message)
parts = [] #: Array[String]

if message.location?
parts << format_location(message)
parts << ': '
end

parts << colorize(message.type.to_s, message.type)
parts << ': '
parts << format_message_text(message.message)

parts.join
end

# @rbs (Message message) -> String
def format_location(message)
return '' unless message.location?

str = "#{message.file}:#{message.line}"

if message.line == message.end_line
if message.column == message.end_column
str += ".#{message.column}"
else
str += ".#{message.column}-#{message.end_column}"
end
else
str += ".#{message.column}-#{message.end_line}.#{message.end_column}"
end

colorize(str, :location)
end

# @rbs (String text) -> String
def format_message_text(text)
text.gsub(/'([^']+)'/) do |_match|
quoted = $1 || ''
"'" + colorize(quoted, :quote) + "'"
end
end

# @rbs (Message message) -> String
def format_source_line(message)
line_num = message.line.to_s.rjust(GUTTER_WIDTH)
gutter = "#{line_num}#{GUTTER_SEPARATOR}"
source = highlight_source(message)

"#{gutter}#{source}"
end

# @rbs (Message message) -> String
def highlight_source(message)
source = message.source_line || ''
return source unless @color_enabled && message.location?

col = (message.column || 1) - 1
end_col = (message.end_column || message.column || 1) - 1

return source if col < 0 || col >= source.length
end_col = [end_col, source.length].min

before = source[0...col] || ''
highlight = source[col...end_col] || ''
after = source[end_col..-1] || ''

"#{before}#{colorize(highlight, :unexpected)}#{after}"
end

# @rbs (Message message) -> String
def format_caret_line(message)
gutter = ' ' * GUTTER_WIDTH + GUTTER_SEPARATOR
padding = leading_whitespace(message)
caret = build_caret(message)

"#{gutter}#{padding}#{colorize(caret, :caret)}"
end

# @rbs (Message message) -> String
def leading_whitespace(message)
source = message.source_line || ''
col = message.column || 0
return '' if col <= 0

prefix = source[0...col] || ''
prefix.gsub(/[^\t]/, ' ')
end

# @rbs (Message message) -> String
def build_caret(message)
length = message.range_length

if length <= 1
'^'
else
'^' + '~' * (length - 1)
end
end

# @rbs (Message message) -> String
def format_fixit_line(message)
gutter = ' ' * GUTTER_WIDTH + GUTTER_SEPARATOR
padding = ' ' * [(message.column || 1) - 1, 0].max
fixit_text = colorize(message.fixit || '', :fixit_insert)

"#{gutter}#{padding}#{fixit_text}"
end

# @rbs (Message note) -> String
def format_note(note)
parts = [] #: Array[String]

if note.location?
parts << format_location(note)
parts << ': '
end

parts << colorize('note', :note)
parts << ': '
parts << note.message

parts.join
end

# @rbs (String? text, Symbol style) -> String
def colorize(text, style)
return text || '' unless @color_enabled

Color.colorize(text || '', style)
end
end
end
end
Loading