Class: Ibex::ErrorMessages::Parser

Inherits:
Object
  • Object
show all
Includes:
ParserV2
Defined in:
lib/ibex/error_messages/parser.rb,
sig/ibex/error_messages/parser.rbs

Overview

Strict line-oriented parser for the ibex-messages v1 and v2 formats.

Constant Summary collapse

ESCAPES =

Returns:

  • (Object)
{ "\\" => "\\", "n" => "\n", "t" => "\t", "r" => "\r" }.freeze

Instance Method Summary collapse

Methods included from ParserV2

#append_v2_message_line, #close_v2_entry, #parse_sentence, #parse_v2, #parse_v2_entry, #parse_v2_opening, #quoted_token_end, #read_sentence_token, #read_v2_entry_body, #read_v2_metadata, #record_v2_entry, #reject_v2_duplicate, #scan_quoted_token, #skip_sentence_space

Constructor Details

#initialize(source, file:) ⇒ Parser

Returns a new instance of Parser.

RBS:

  • (String source, file: String) -> void

Parameters:

  • source (String)
  • file: (String)


17
18
19
20
21
22
23
# File 'lib/ibex/error_messages/parser.rb', line 17

def initialize(source, file:)
  @file = file
  text = source.dup.force_encoding(Encoding::UTF_8)
  fail_at(1, 1, "messages file must be valid UTF-8") unless text.valid_encoding?

  @lines = text.lines(chomp: true)
end

Instance Method Details

#close_entry(message_lines, opening_index, index, state, status) ⇒ [ Entry, Integer ]

RBS:

  • (Array[String] message_lines, Integer opening_index, Integer index, Integer state, :active | :removed status) -> [Entry, Integer]

Parameters:

  • message_lines (Array[String])
  • opening_index (Integer)
  • index (Integer)
  • state (Integer)
  • status (:active, :removed)

Returns:



93
94
95
96
97
98
99
# File 'lib/ibex/error_messages/parser.rb', line 93

def close_entry(message_lines, opening_index, index, state, status)
  message = message_lines.empty? ? nil : message_lines.join("\n")
  fail_at(opening_index + 1, 1, "message for state #{state} must not be empty") if message&.strip&.empty?

  entry = Entry.new(state: state, status: status, message: message, line: opening_index + 1)
  [entry, index + 1]
end

#column(line) ⇒ Integer

RBS:

  • (String line) -> Integer

Parameters:

  • line (String)

Returns:

  • (Integer)


149
150
151
# File 'lib/ibex/error_messages/parser.rb', line 149

def column(line)
  (line.index(/\S/) || 0) + 1
end

#decode_line(content, line, content_column) ⇒ String

RBS:

  • (String content, Integer line, Integer content_column) -> String

Parameters:

  • content (String)
  • line (Integer)
  • content_column (Integer)

Returns:

  • (String)


116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/ibex/error_messages/parser.rb', line 116

def decode_line(content, line, content_column)
  decoded = +""
  index = 0
  while index < content.length
    character = content[index] || raise(Ibex::Error, "missing message character")
    unless character == "\\"
      decoded << character
      index += 1
      next
    end

    escaped = content[index + 1]
    fail_at(line, content_column + index, "trailing backslash in message line") unless escaped
    replacement = ESCAPES[escaped]
    fail_at(line, content_column + index, "unknown escape \\#{escaped}") unless replacement

    decoded << replacement
    index += 2
  end
  decoded
end

#fail_at(line, column, message) ⇒ bot

RBS:

  • (Integer line, Integer column, String message) -> bot

Parameters:

  • line (Integer)
  • column (Integer)
  • message (String)

Returns:

  • (bot)


154
155
156
# File 'lib/ibex/error_messages/parser.rb', line 154

def fail_at(line, column, message)
  raise Ibex::Error, "#{@file}:#{line}:#{column}: #{message}"
end

#ignorable?(line) ⇒ Boolean

RBS:

  • (String line) -> bool

Parameters:

  • line (String)

Returns:

  • (Boolean)


139
140
141
# File 'lib/ibex/error_messages/parser.rb', line 139

def ignorable?(line)
  line.strip.empty? || line.lstrip.start_with?("#")
end

#parseDocument

RBS:

  • () -> Document

Returns:



26
27
28
29
30
31
# File 'lib/ibex/error_messages/parser.rb', line 26

def parse
  version = validate_header
  return parse_v2 if version == 2

  parse_v1
end

#parse_entry(opening_index, state, status) ⇒ [ Entry, Integer ]

RBS:

  • (Integer opening_index, Integer state, :active | :removed status) -> [Entry, Integer]

Parameters:

  • opening_index (Integer)
  • state (Integer)
  • status (:active, :removed)

Returns:



77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/ibex/error_messages/parser.rb', line 77

def parse_entry(opening_index, state, status)
  message_lines = [] #: Array[String]
  index = opening_index + 1
  while index < @lines.length
    line = @lines.fetch(index)
    return close_entry(message_lines, opening_index, index, state, status) if line.strip == "end"

    read_entry_line(message_lines, line, index)
    index += 1
  end
  label = status == :active ? "state" : "removed"
  fail_at(opening_index + 1, 1, "unterminated #{label} #{state} entry")
end

#parse_v1Document

RBS:

  • () -> Document

Returns:



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/ibex/error_messages/parser.rb', line 36

def parse_v1
  entries = [] #: Array[Entry]
  declarations = {} #: Hash[Integer, Integer]
  index = 1
  while index < @lines.length
    line = @lines.fetch(index)
    if ignorable?(line)
      index += 1
      next
    end

    match = line.strip.match(/\A(state|removed)\s+([0-9]+)\z/)
    fail_at(index + 1, column(line), top_level_expectation) unless match
    state_text = match[2] || raise(Ibex::Error, "missing state number")
    state = Integer(state_text, 10)
    reject_duplicate(declarations, state, index, line)
    declarations[state] = index + 1
    entry, index = parse_entry(index, state, match[1] == "state" ? :active : :removed)
    entries << entry
  end
  Document.new(version: 1, entries: entries)
end

#read_entry_line(message_lines, line, index) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] message_lines, String line, Integer index) -> void

Parameters:

  • message_lines (Array[String])
  • line (String)
  • index (Integer)


102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/ibex/error_messages/parser.rb', line 102

def read_entry_line(message_lines, line, index)
  return if ignorable?(line)

  unless line.start_with?("|")
    fail_at(index + 1, column(line), "expected a `| ` message line, a comment, a blank line, or `end`")
  end

  content = line.delete_prefix("|")
  content = content.delete_prefix(" ")
  content_column = line.start_with?("| ") ? 3 : 2
  message_lines << decode_line(content, index + 1, content_column)
end

#reject_duplicate(declarations, state, index, line) ⇒ void

This method returns an undefined value.

RBS:

  • (Hash[Integer, Integer] declarations, Integer state, Integer index, String line) -> void

Parameters:

  • declarations (Hash[Integer, Integer])
  • state (Integer)
  • index (Integer)
  • line (String)


69
70
71
72
73
74
# File 'lib/ibex/error_messages/parser.rb', line 69

def reject_duplicate(declarations, state, index, line)
  return unless declarations.key?(state)

  fail_at(index + 1, column(line),
          "duplicate state #{state}; first declared at line #{declarations.fetch(state)}")
end

#top_level_expectationString

RBS:

  • () -> String

Returns:

  • (String)


144
145
146
# File 'lib/ibex/error_messages/parser.rb', line 144

def top_level_expectation
  "expected `state N`, `removed N`, a comment, or a blank line"
end

#validate_headerInteger

RBS:

  • () -> Integer

Returns:

  • (Integer)


60
61
62
63
64
65
66
# File 'lib/ibex/error_messages/parser.rb', line 60

def validate_header
  header = @lines.first&.delete_prefix("\uFEFF")
  return 1 if header == HEADER_V1
  return 2 if header == HEADER

  fail_at(1, 1, "expected #{HEADER.inspect} or #{HEADER_V1.inspect}")
end