module Steep class Expectations class Diagnostic < Struct.new(:start_position, :end_position, :severity, :message, :code, keyword_init: true) DiagnosticSeverity = LanguageServer::Protocol::Constant::DiagnosticSeverity def self.from_hash(hash) start_position = { line: hash.dig("range", "start", "line") - 1, character: hash.dig("range", "start", "character") } #: position end_position = { line: hash.dig("range", "end", "line") - 1, character: hash.dig("range", "end", "character") } #: position severity = case hash["severity"] || "ERROR" when "ERROR" :error when "WARNING" :warning when "INFORMATION" :information when "HINT" :hint end #: Diagnostic::LSPFormatter::severity Diagnostic.new( start_position: start_position, end_position: end_position, severity: severity, message: hash["message"], code: hash["code"] ) end def self.from_lsp(diagnostic) start_position = { line: diagnostic.dig(:range, :start, :line), character: diagnostic.dig(:range, :start, :character) } #: position end_position = { line: diagnostic.dig(:range, :end, :line), character: diagnostic.dig(:range, :end, :character) } #: position severity = case diagnostic[:severity] when DiagnosticSeverity::ERROR :error when DiagnosticSeverity::WARNING :warning when DiagnosticSeverity::INFORMATION :information when DiagnosticSeverity::HINT :hint else :error end #: Diagnostic::LSPFormatter::severity Diagnostic.new( start_position: start_position, end_position: end_position, severity: severity, message: diagnostic[:message], code: diagnostic[:code] ) end def to_hash { "range" => { "start" => { "line" => start_position[:line] + 1, "character" => start_position[:character] }, "end" => { "line" => end_position[:line] + 1, "character" => end_position[:character] } }, "severity" => severity.to_s.upcase, "message" => message, "code" => code } end def lsp_severity case severity when :error DiagnosticSeverity::ERROR when :warning DiagnosticSeverity::WARNING when :information DiagnosticSeverity::INFORMATION when :hint DiagnosticSeverity::HINT else raise end end def to_lsp { range: { start: { line: start_position[:line], character: start_position[:character] }, end: { line: end_position[:line], character: end_position[:character] } }, severity: lsp_severity, message: message, code: code } end def sort_key [ start_position[:line], start_position[:character], end_position[:line], end_position[:character], code, severity, message ] end end class TestResult attr_reader :path attr_reader :expectation attr_reader :actual def initialize(path:, expectation:, actual:) @path = path @expectation = expectation @actual = actual end def empty? actual.empty? end def satisfied? unexpected_diagnostics.empty? && missing_diagnostics.empty? end def each_diagnostics if block_given? expected_set = Set.new(expectation) #: Set[Diagnostic] actual_set = Set.new(actual) #: Set[Diagnostic] (expected_set + actual_set).sort_by(&:sort_key).each do |diagnostic| case when expected_set.include?(diagnostic) && actual_set.include?(diagnostic) yield [:expected, diagnostic] when expected_set.include?(diagnostic) yield [:missing, diagnostic] when actual_set.include?(diagnostic) yield [:unexpected, diagnostic] end end else enum_for :each_diagnostics end end def expected_diagnostics each_diagnostics.select {|type, _| type == :expected }.map {|_, diag| diag } end def unexpected_diagnostics each_diagnostics.select {|type, _| type == :unexpected }.map {|_, diag| diag } end def missing_diagnostics each_diagnostics.select {|type, _| type == :missing }.map {|_, diag| diag } end end LSP = LanguageServer::Protocol attr_reader :diagnostics def initialize() @diagnostics = {} end def test(path:, diagnostics:) TestResult.new(path: path, expectation: self.diagnostics[path] || [], actual: diagnostics) end def self.empty new() end def to_yaml array = [] #: Array[{ "file" => String, "diagnostics" => Array[untyped] }] diagnostics.each_key.sort.each do |key| ds = diagnostics[key] array << { "file" => key.to_s, 'diagnostics' => ds.sort_by(&:sort_key).map(&:to_hash) } end YAML.dump(array) end def self.load(path:, content:) expectations = new() YAML.load(content, filename: path.to_s).each do |entry| file = Pathname(entry["file"]) expectations.diagnostics[file] = entry["diagnostics"].map {|hash| Diagnostic.from_hash(hash) }.sort_by!(&:sort_key) end expectations end end end