Class: Ibex::Impact::Report

Inherits:
Object
  • Object
show all
Defined in:
lib/ibex/impact/report.rb,
sig/ibex/impact/report.rbs

Overview

Builds the deterministic public impact-v1 document.

Instance Method Summary collapse

Constructor Details

#initialize(mode:, algorithm:, grammar:, before:, after:, seeds:, nodes:, symbol_kinds:, set_changes:, automaton:, actions:, coverage:, metadata_names: [], minimum: "info", warnings: []) ⇒ Report

rubocop:disable Metrics/ParameterLists

RBS:

  • @mode: String

  • @algorithm: String

  • @grammar: IR::Grammar

  • @before: IR::Automaton?

  • @after: IR::Automaton?

  • @seeds: Array[Hash[Symbol, Object?]]

  • @nodes: Hash[Integer, Node]

  • @symbol_kinds: Hash[Integer, Array[String]]

  • @set_changes: Hash[String, Hash[Symbol, Object?]]

  • @metadata_names: Array[String]

  • @automaton: Hash[Symbol, Object?]

  • @actions: Array[Hash[Symbol, Object?]]

  • @coverage: Hash[Symbol, Object?]

  • @minimum: String

  • @warnings: Array[String]

  • (mode: String, algorithm: String, grammar: IR::Grammar, before: IR::Automaton?, after: IR::Automaton?, seeds: Array[Hash[Symbol, Object?]], nodes: Hash[Integer, Node], symbol_kinds: Hash[Integer, Array[String]], set_changes: Hash[String, Hash[Symbol, Object?]], automaton: Hash[Symbol, Object?], actions: Array[Hash[Symbol, Object?]], coverage: Hash[Symbol, Object?], ?metadata_names: Array[String], ?minimum: String, ?warnings: Array[String]) -> void

Parameters:

  • mode: (String)
  • algorithm: (String)
  • grammar: (IR::Grammar)
  • before: (IR::Automaton, nil)
  • after: (IR::Automaton, nil)
  • seeds: (Array[Hash[Symbol, Object?]])
  • nodes: (Hash[Integer, Node])
  • symbol_kinds: (Hash[Integer, Array[String]])
  • set_changes: (Hash[String, Hash[Symbol, Object?]])
  • automaton: (Hash[Symbol, Object?])
  • actions: (Array[Hash[Symbol, Object?]])
  • coverage: (Hash[Symbol, Object?])
  • metadata_names: (Array[String]) (defaults to: [])
  • minimum: (String) (defaults to: "info")
  • warnings: (Array[String]) (defaults to: [])


33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/ibex/impact/report.rb', line 33

def initialize(mode:, algorithm:, grammar:, before:, after:, seeds:, nodes:, symbol_kinds:, set_changes:,
               automaton:,
               actions:, coverage:, metadata_names: [], minimum: "info", warnings: [])
  @mode = mode
  @algorithm = algorithm
  @grammar = grammar
  @before = before
  @after = after
  @seeds = seeds
  @nodes = nodes
  @symbol_kinds = symbol_kinds
  @set_changes = set_changes
  @metadata_names = .uniq.sort
  @automaton = automaton #: Hash[Symbol, Object?]
  @actions = actions #: Array[Hash[Symbol, Object?]]
  @coverage = coverage #: Hash[Symbol, Object?]
  @minimum = minimum
  @warnings = warnings
end

Instance Method Details

#action_documents(minimum) ⇒ Array[Hash[Symbol, Object?]]

RBS:

  • (String minimum) -> Array[Hash[Symbol, Object?]]

Parameters:

  • minimum (String)

Returns:

  • (Array[Hash[Symbol, Object?]])


202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/ibex/impact/report.rb', line 202

def action_documents(minimum)
  documents = @actions.sort_by { |action| action_production(action) }.map do |action|
    production = action_production(action)
    severity = action.fetch(:severity) #: String
    reason = action.fetch(:reason) #: String
    context_length = action.fetch(:context_length) #: Hash[Symbol, Integer]
    location = action.fetch(:loc, nil) #: IR::location?
    {
      production: production, severity: severity, reason: reason,
      context_length: context_length, loc: stable_location(location)
    }
  end
  documents.select { |action| Severity::RANK.fetch(severity_of(action)) >= Severity::RANK.fetch(minimum) }
end

#action_production(action) ⇒ String

RBS:

  • (Hash[Symbol, Object?] action) -> String

Parameters:

  • action (Hash[Symbol, Object?])

Returns:

  • (String)


218
219
220
# File 'lib/ibex/impact/report.rb', line 218

def action_production(action)
  action.fetch(:production) #: String
end

#action_severity(name, current) ⇒ String

RBS:

  • (String name, String current) -> String

Parameters:

  • name (String)
  • current (String)

Returns:

  • (String)


145
146
147
148
# File 'lib/ibex/impact/report.rb', line 145

def action_severity(name, current)
  findings = @actions.select { |action| action_production(action).start_with?("#{name} ->") }
  findings.reduce(current) { |level, finding| Severity.max(level, severity_of(finding)) }
end

#change_kinds(name) ⇒ Array[String]

RBS:

  • (String) -> Array[String]

Parameters:

  • (String)

Returns:

  • (Array[String])


109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/ibex/impact/report.rb', line 109

def change_kinds(name)
  change = @set_changes[name]
  return [] unless change

  kinds = [] #: Array[String]
  first = change.fetch(:first, empty_change)
  follow = change.fetch(:follow, empty_change)
  kinds << "first" unless first.fetch(:added).empty? && first.fetch(:removed).empty?
  kinds << "follow" unless follow.fetch(:added).empty? && follow.fetch(:removed).empty?
  kinds << "nullable" if change.fetch(:nullable, nil)
  kinds
end

#component_names(node, name) ⇒ Array[String]

RBS:

  • (Node?, String) -> Array[String]

Parameters:

  • (Node, nil)
  • (String)

Returns:

  • (Array[String])


129
130
131
132
133
# File 'lib/ibex/impact/report.rb', line 129

def component_names(node, name)
  return [name] unless node

  node.component.sort.map { |member| @grammar.symbol_by_id(member)&.name }.compact.sort
end

#coverage_documentHash[Symbol, Object?]

RBS:

  • () -> Hash[Symbol, Object?]

Returns:

  • (Hash[Symbol, Object?])


223
224
225
226
227
228
229
230
231
232
# File 'lib/ibex/impact/report.rb', line 223

def coverage_document
  empty_reports = [] #: Array[Hash[Symbol, Object?]]
  reports = @coverage.fetch(:reports, empty_reports) #: Array[Hash[Symbol, Object?]]
  status = @coverage.fetch(:status) #: String
  { status: status,
    reports: reports.sort_by { |report| coverage_report_path(report) }.map do |report|
      productions = report.fetch(:productions) #: Array[Integer]
      { path: stable_path(coverage_report_path(report)), productions: productions.sort }
    end }
end

#coverage_report_path(report) ⇒ String

RBS:

  • (Hash[Symbol, Object?] report) -> String

Parameters:

  • report (Hash[Symbol, Object?])

Returns:

  • (String)


235
236
237
# File 'lib/ibex/impact/report.rb', line 235

def coverage_report_path(report)
  report.fetch(:path) #: String
end

#document_severity(name, kinds) ⇒ String

RBS:

  • (String, Array[String]) -> String

Parameters:

  • (String)
  • (Array[String])

Returns:

  • (String)


123
124
125
126
# File 'lib/ibex/impact/report.rb', line 123

def document_severity(name, kinds)
  level = kinds.reduce("info") { |current, kind| Severity.max(current, kind_severity(kind)) }
  action_severity(name, level)
end

#empty_changeHash[Symbol, Array[String]]

RBS:

  • () -> Hash[Symbol, Array[String]]

Returns:

  • (Hash[Symbol, Array[String]])


156
157
158
# File 'lib/ibex/impact/report.rb', line 156

def empty_change
  { added: [], removed: [] }
end

#kind_severity(kind) ⇒ String

RBS:

  • (String kind) -> String

Parameters:

  • kind (String)

Returns:

  • (String)


136
137
138
139
140
141
142
# File 'lib/ibex/impact/report.rb', line 136

def kind_severity(kind)
  return "high" if %w[first follow nullable action_arity].include?(kind)
  return "medium" if %w[reference precedence].include?(kind)
  return "low" if kind == "metadata"

  "info"
end

#production_shape(production) ⇒ String?

RBS:

  • (IR::Production?) -> String?

Parameters:

Returns:

  • (String, nil)


178
179
180
181
182
183
184
# File 'lib/ibex/impact/report.rb', line 178

def production_shape(production)
  return unless production

  lhs = symbol_name(production.lhs)
  rhs = production.rhs.map { |id| symbol_name(id) }
  "#{lhs} -> #{rhs.join(' ')}"
end

#reportable_symbol?(name) ⇒ Boolean

RBS:

  • (String) -> bool

Parameters:

  • (String)

Returns:

  • (Boolean)


96
97
98
# File 'lib/ibex/impact/report.rb', line 96

def reportable_symbol?(name)
  !!(@grammar.symbol(name) || @set_changes.key?(name) || @metadata_names.include?(name))
end

#severity_of(record) ⇒ String

RBS:

  • (Hash[Symbol, Object?] record) -> String

Parameters:

  • record (Hash[Symbol, Object?])

Returns:

  • (String)


151
152
153
# File 'lib/ibex/impact/report.rb', line 151

def severity_of(record)
  record.fetch(:severity) #: String
end

#stable_grammar_digest(automaton) ⇒ String?

RBS:

  • (IR::Automaton?) -> String?

Parameters:

Returns:

  • (String, nil)


256
257
258
259
260
261
262
263
# File 'lib/ibex/impact/report.rb', line 256

def stable_grammar_digest(automaton)
  return unless automaton

  serialized = IR::Serialize.dump(automaton.grammar)
  canonical = serialized.gsub(/"file":\s*"[^"]*"/, '"file": "<source>"')
  canonical = canonical.gsub(/"root":\s*"[^"]*"/, '"root": null')
  "sha256:#{Digest::SHA256.hexdigest(canonical)}"
end

#stable_location(location) ⇒ Hash[Symbol, Integer]?

RBS:

  • (IR::location?) -> Hash[Symbol, Integer]?

Parameters:

  • (IR::location, nil)

Returns:

  • (Hash[Symbol, Integer], nil)


249
250
251
252
253
# File 'lib/ibex/impact/report.rb', line 249

def stable_location(location)
  return unless location

  { line: location[:line], column: location[:column] }
end

#stable_path(path) ⇒ String

RBS:

  • (String) -> String

Parameters:

  • (String)

Returns:

  • (String)


266
267
268
269
270
# File 'lib/ibex/impact/report.rb', line 266

def stable_path(path)
  return path unless path.start_with?(File::SEPARATOR)

  File.basename(path)
end

#stable_warningsArray[String]

RBS:

  • () -> Array[String]

Returns:

  • (Array[String])


240
241
242
243
244
245
246
# File 'lib/ibex/impact/report.rb', line 240

def stable_warnings
  empty_reports = [] #: Array[Hash[Symbol, Object?]]
  paths = @coverage.fetch(:reports, empty_reports).map { |report| coverage_report_path(report) }
  @warnings.map do |warning|
    paths.reduce(warning) { |current, path| current.gsub(path, stable_path(path)) }
  end.sort.uniq
end

#symbol_document(name) ⇒ Hash[Symbol, Object?]?

RBS:

  • (String) -> Hash[Symbol, Object?]?

Parameters:

  • (String)

Returns:

  • (Hash[Symbol, Object?], nil)


79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/ibex/impact/report.rb', line 79

def symbol_document(name)
  id = @grammar.symbol(name)&.id
  node = id && @nodes[id]
  return unless reportable_symbol?(name)

  kinds = symbol_kinds_for(name, id)
  kinds = ["reference"] if kinds.empty? && node
  severity = document_severity(name, kinds)
  {
    symbol: name, severity: severity, kinds: kinds, distance: node&.distance || 0,
    component: component_names(node, name),
    sets: @set_changes.fetch(name, { first: empty_change, follow: empty_change, nullable: nil }),
    witness: witness_documents(node&.witness || [])
  }
end

#symbol_documents(minimum) ⇒ Array[Hash[Symbol, Object?]]

RBS:

  • (String minimum) -> Array[Hash[Symbol, Object?]]

Parameters:

  • minimum (String)

Returns:

  • (Array[Hash[Symbol, Object?]])


71
72
73
74
75
76
# File 'lib/ibex/impact/report.rb', line 71

def symbol_documents(minimum)
  names = (@nodes.keys.filter_map { |id| @grammar.symbol_by_id(id)&.name } + @set_changes.keys + @metadata_names)
          .uniq.sort
  documents = names.filter_map { |name| symbol_document(name) }
  documents.select { |document| Severity::RANK.fetch(severity_of(document)) >= Severity::RANK.fetch(minimum) }
end

#symbol_kinds_for(name, id) ⇒ Array[String]

RBS:

  • (String, Integer?) -> Array[String]

Parameters:

  • (String)
  • (Integer, nil)

Returns:

  • (Array[String])


101
102
103
104
105
106
# File 'lib/ibex/impact/report.rb', line 101

def symbol_kinds_for(name, id)
  kinds = id ? (@symbol_kinds[id] || []).dup : [] #: Array[String]
  kinds << "metadata" if @metadata_names.include?(name)
  kinds.concat(change_kinds(name))
  kinds.uniq.sort
end

#symbol_name(id) ⇒ String

RBS:

  • (Integer) -> String

Parameters:

  • (Integer)

Returns:

  • (String)


173
174
175
# File 'lib/ibex/impact/report.rb', line 173

def symbol_name(id)
  @grammar.symbol_by_id(id)&.name || id.to_s
end

#to_h(minimum: @minimum) ⇒ Hash[Symbol, Object?]

RBS:

  • (?minimum: String) -> Hash[Symbol, Object?]

Parameters:

  • minimum: (String) (defaults to: @minimum)

Returns:

  • (Hash[Symbol, Object?])


55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/ibex/impact/report.rb', line 55

def to_h(minimum: @minimum)
  symbols = symbol_documents(minimum)
  action_records = action_documents(minimum)
  {
    ibex_report: "impact", schema_version: 1, mode: @mode, algorithm: @algorithm,
    grammar_digest: { before: stable_grammar_digest(@before), after: stable_grammar_digest(@after) },
    seeds: @seeds.map { |seed| seed.slice(:symbol, :origin, :nullable_boundary) },
    symbols: symbols, automaton: @automaton, actions: action_records,
    coverage: coverage_document, warnings: stable_warnings,
    totals: totals(symbols, action_records, @automaton)
  }
end

#totals(symbols, actions, automaton) ⇒ Hash[Symbol, Integer]

RBS:

  • (Array[Hash[Symbol, Object?]], Array[Hash[Symbol, Object?]], Hash[Symbol, Object?]) -> Hash[Symbol, Integer]

Parameters:

  • (Array[Hash[Symbol, Object?]])
  • (Array[Hash[Symbol, Object?]])
  • (Hash[Symbol, Object?])

Returns:

  • (Hash[Symbol, Integer])


188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/ibex/impact/report.rb', line 188

def totals(symbols, actions, automaton)
  counts = Severity::LEVELS.to_h { |level| [level.to_sym, 0] }
  symbols.each { |item| counts[item.fetch(:severity).to_s.to_sym] += 1 }
  actions.each { |item| counts[item.fetch(:severity).to_s.to_sym] += 1 }
  conflicts = automaton.dig(:conflicts, :added) || [] #: Array[Hash[Symbol, Object?]]
  counts[:critical] += conflicts.length
  unreachable = automaton.fetch(:unreachable, []) #: Array[Integer]
  counts[:critical] += unreachable.length
  unreachable_nonterminals = automaton.fetch(:unreachable_nonterminals, []) #: Array[String]
  counts[:critical] += unreachable_nonterminals.length
  counts
end

#witness_documents(witness) ⇒ Array[Hash[Symbol, Object?]]

RBS:

  • (Array[Edge]) -> Array[Hash[Symbol, Object?]]

Parameters:

Returns:

  • (Array[Hash[Symbol, Object?]])


161
162
163
164
165
166
167
168
169
170
# File 'lib/ibex/impact/report.rb', line 161

def witness_documents(witness)
  witness.map do |edge|
    production = edge.production && @grammar.productions[edge.production]
    location = production&.origin&.fetch(:loc, nil) #: IR::location?
    {
      from: symbol_name(edge.source), to: symbol_name(edge.target), production: production_shape(production),
      loc: stable_location(location)
    }
  end
end