diagnostics upgrade

This commit is contained in:
2026-07-15 22:55:31 +02:00
parent c4fa8e930f
commit 7de0b7f268
11 changed files with 857 additions and 75 deletions
+222 -15
View File
@@ -917,8 +917,8 @@ multi_source_diagnostics_report_the_originating_file :: proc(t: ^testing.T) {
found := false
for _, diagnostic_index in diagnostics.items {
message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index))
if strings.contains(message, "/b.bro:2:9:") &&
strings.contains(message, "unknown package alias 'math'") {
if strings.contains(message, "--> ") && strings.contains(message, "/b.bro:2:9") &&
strings.contains(message, "unknown symbol 'math'") {
found = true
}
delete(message)
@@ -1956,7 +1956,7 @@ old_intrinsic_spellings_are_not_recognized :: proc(t: ^testing.T) {
}
found := [len(names)]bool{}
for diagnostic in diagnostics.items {
if !strings.contains(diagnostic.message, "unresolved function") {
if !strings.contains(diagnostic.message, "unknown symbol") {
continue
}
for name, index in names {
@@ -4306,6 +4306,34 @@ main func() i32 {
testing.expect(t, indirect_calls > 0)
}
@(test)
qualified_value_calls_do_not_imply_package_resolution :: proc(t: ^testing.T) {
text := `Callbacks :: struct {
call @func() void
}
main func() void {
callbacks Callbacks = Callbacks{call = func() void {}}
callbacks.call()
missing.call()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 1)
testing.expect_value(t, diagnostics.items[0].message, "unknown symbol 'missing'")
testing.expect(t, !strings.contains(diagnostics.items[0].message, "package"))
}
@(test)
function_literals_do_not_capture_locals :: proc(t: ^testing.T) {
text := `main func() i32 {
@@ -4330,7 +4358,7 @@ function_literals_do_not_capture_locals :: proc(t: ^testing.T) {
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "unresolved global 'offset'")
found = found || strings.contains(diagnostic.message, "unknown symbol 'offset'")
}
testing.expect(t, found)
}
@@ -7210,7 +7238,7 @@ source_store_owns_buffers_indexes_lines_and_deduplicates_diagnostics :: proc(t:
testing.expect_value(t, len(store.items[source_id].line_starts), 3)
testing.expect_value(t, first, second)
testing.expect_value(t, len(diagnostics.items), 2)
testing.expect(t, strings.contains(formatted, "owned.bro:2:1:"))
testing.expect(t, strings.contains(formatted, "--> owned.bro:2:1"))
}
@(test)
@@ -7233,8 +7261,92 @@ diagnostic_warnings_format_and_dedupe_by_severity :: proc(t: ^testing.T) {
testing.expect_value(t, len(diagnostics.items), 2)
testing.expect_value(t, diagnostics.items[warning].severity, source.Severity.Warning)
testing.expect_value(t, diagnostics.items[err].severity, source.Severity.Error)
testing.expect(t, strings.contains(formatted_warning, "test.bro:1:1: warning: same"))
testing.expect(t, strings.contains(formatted_error, "test.bro:1:1: error: same"))
testing.expect(t, strings.contains(formatted_warning, "warning: same\n --> test.bro:1:1"))
testing.expect(t, strings.contains(formatted_error, "error: same\n --> test.bro:1:1"))
}
@(test)
rich_diagnostics_render_labels_notes_help_and_tabs :: proc(t: ^testing.T) {
store := source.init_store()
defer source.destroy_store(&store)
primary_file := source.add_source(&store, "main.bro", "\tmissing()\nnext()\n")
definition_file := source.add_source(&store, "dep.bro", "value :: 1\n")
diagnostics := source.init_store_diagnostics(&store)
defer source.destroy_diagnostics(&diagnostics)
id := source.add(&diagnostics, source.Span{file=primary_file, start=1, end=8}, "unknown symbol 'missing'")
source.set_primary_label(&diagnostics, id, "unknown symbol")
source.set_primary_label(&diagnostics, id, "unknown symbol")
source.add_secondary_label(
&diagnostics, id,
source.Span{file=definition_file, start=0, end=5},
"related declaration",
)
source.add_note(&diagnostics, id, "names resolve in the current scope")
source.add_note(&diagnostics, id, "names resolve in the current scope")
source.add_help(&diagnostics, id, "declare 'missing' before using it")
formatted := source.format(&diagnostics, id)
defer delete(formatted)
multiline := source.add(
&diagnostics,
source.Span{file=primary_file, start=1, end=17},
"multiline failure",
)
multiline_formatted := source.format(&diagnostics, multiline)
defer delete(multiline_formatted)
unknown := source.add(&diagnostics, source.Span{}, "no location")
unknown_formatted := source.format(&diagnostics, unknown)
defer delete(unknown_formatted)
testing.expect_value(t, len(diagnostics.annotations), 4)
testing.expect(t, strings.contains(formatted, "error: unknown symbol 'missing'"))
testing.expect(t, strings.contains(formatted, "--> main.bro:1:2"))
testing.expect(t, strings.contains(formatted, "^^^^^^^ unknown symbol"))
testing.expect(t, strings.contains(formatted, "::: dep.bro:1:1"))
testing.expect(t, strings.contains(formatted, "----- related declaration"))
testing.expect(t, strings.contains(formatted, "note: names resolve in the current scope"))
testing.expect(t, strings.contains(formatted, "help: declare 'missing' before using it"))
testing.expect(t, strings.contains(multiline_formatted, "1 | missing()"))
testing.expect(t, !strings.contains(multiline_formatted, "next()"))
testing.expect_value(t, unknown_formatted, "main.bro: error: no location")
}
@(test)
poisoned_expressions_preserve_independent_root_diagnostics :: proc(t: ^testing.T) {
text := `bad :: missing_global
sink func(value i32) void { _ = value }
broken func(value int) int { return missing_return + value }
main func() void {
_ = missing_add + 1
_ = try missing_try()
missing_catch() catch |_| {}
sink(missing_arg)
missing_stmt()
missing_target.field = 1
value i32 = 0
value += missing_rhs
_ = broken(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 9)
for diagnostic in diagnostics.items {
testing.expect(t, strings.has_prefix(diagnostic.message, "unknown symbol 'missing_"))
testing.expect(t, !strings.contains(diagnostic.message, "fallible expression"))
testing.expect(t, !strings.contains(diagnostic.message, "must be consumed"))
testing.expect(t, !strings.contains(diagnostic.message, "compatible numeric operands"))
testing.expect(t, !strings.contains(diagnostic.message, "implicitly convert"))
}
}
@(test)
@@ -7923,7 +8035,7 @@ referenced_missing_package_traps_at_reference :: proc(t: ^testing.T) {
defer delete(stdout)
defer delete(stderr)
testing.expect(t, !state.success)
testing.expect(t, strings.contains(string(stderr), "/missing_used/app/main.bro:4:6:"))
testing.expect(t, strings.contains(string(stderr), "/missing_used/app/main.bro:4:6"))
}
@(test)
@@ -7991,8 +8103,8 @@ file_hidden_declarations_are_not_package_members :: proc(t: ^testing.T) {
found_import := false
found_collision := false
for diagnostic in diagnostics.items {
found_sibling = found_sibling || strings.contains(diagnostic.message, "unresolved function 'sibling'")
found_sibling_value = found_sibling_value || strings.contains(diagnostic.message, "unresolved global 'sibling_value'")
found_sibling = found_sibling || strings.contains(diagnostic.message, "unknown symbol 'sibling'")
found_sibling_value = found_sibling_value || strings.contains(diagnostic.message, "unknown symbol 'sibling_value'")
found_sibling_type = found_sibling_type || strings.contains(diagnostic.message, "unknown or opaque record type 'Sibling'")
found_import = found_import || strings.contains(diagnostic.message, "package 'dep' has no member 'secret'")
found_collision = found_collision || strings.contains(diagnostic.message, "duplicate function 'collision'")
@@ -8221,7 +8333,7 @@ main func() void {}
wants := []string{
"has no member 'missing'",
"is file-hidden",
"unknown package alias 'nope'",
"unknown symbol 'nope'",
"unavailable imported package 'gone'",
"package member 'dep.ambiguous' is ambiguous",
"duplicate declaration alias 'duplicate'",
@@ -8937,10 +9049,10 @@ conditional_unwrap_diagnostics_cover_counts_guards_and_capture_scope :: proc(t:
duplicate = duplicate || strings.contains(diagnostic.message, "unwrap captures must have distinct names")
non_optional = non_optional || strings.contains(diagnostic.message, "unwrap requires an optional value")
guard = guard || strings.contains(diagnostic.message, "unwrap guard must be a bool")
outer_operand_scope = outer_operand_scope || strings.contains(diagnostic.message, "unresolved global 'earlier'")
outer_operand_scope = outer_operand_scope || strings.contains(diagnostic.message, "unknown symbol 'earlier'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'value'")
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'value'")
capture_scope += 1 if strings.contains(diagnostic.message, "unresolved global 'value'") else 0
capture_scope += 1 if strings.contains(diagnostic.message, "unknown symbol 'value'") else 0
}
testing.expect_value(t, count_mismatches, 2)
testing.expect(t, duplicate)
@@ -8978,7 +9090,7 @@ if_unwrap_binding_is_scoped_to_then_block :: proc(t: ^testing.T) {
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "unresolved global 'v'")
found = found || strings.contains(diagnostic.message, "unknown symbol 'v'")
}
testing.expect(t, found)
}
@@ -9433,7 +9545,7 @@ main func() void {
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'item'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'item'")
immutable_pointer = immutable_pointer || strings.contains(diagnostic.message, "assignment target is not writable")
scope = scope || strings.contains(diagnostic.message, "unresolved global 'item'")
scope = scope || strings.contains(diagnostic.message, "unknown symbol 'item'")
integer_bounds += 1 if strings.contains(diagnostic.message, "range bounds must be compatible concrete integers") else 0
}
testing.expect(t, unsupported)
@@ -11920,6 +12032,101 @@ main func() void {
testing.expect(t, found)
}
@(test)
missing_qualified_signature_symbol_reports_one_root_error :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-root-diagnostic"
defer _ = os2.remove_all(directory)
_ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
token_text := `Token :: struct { start int }
`
lexer_text := `scan func(cursor usize) void ! missing.Error {
token Token = Token{start = cursor}
_ = token
}
`
main_text := `main func() void {
scan(1) catch |_| { return }
}
`
testing.expect(t, os.write_entire_file(
"/tmp/brolang-test-root-diagnostic/token.bro",
transmute([]byte)token_text,
))
testing.expect(t, os.write_entire_file(
"/tmp/brolang-test-root-diagnostic/lexer.bro",
transmute([]byte)lexer_text,
))
testing.expect(t, os.write_entire_file(
"/tmp/brolang-test-root-diagnostic/main.bro",
transmute([]byte)main_text,
))
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load(directory, &sources, &diagnostics, &symbols)
defer ast.destroy_module(&module)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 1)
testing.expect_value(t, diagnostics.items[0].message, "unknown symbol 'missing'")
formatted := source.format(&diagnostics, source.Diagnostic_Id(0))
defer delete(formatted)
testing.expect(t, strings.contains(formatted, "/lexer.bro:1:32"))
testing.expect(t, strings.contains(formatted, "^^^^^^^ unknown symbol"))
testing.expect(t, !strings.contains(formatted, "fallible expression"))
testing.expect(t, !strings.contains(formatted, "must be consumed"))
testing.expect(t, !strings.contains(formatted, "could not resolve the 'int' constraint"))
}
@(test)
poisoned_global_and_local_types_do_not_create_inference_fallbacks :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-poisoned-declarations"
defer _ = os2.remove_all(directory)
_ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
text := `bad missing.Global :: 1
main func() void {
value absent.Local = 1
_ = value
}
`
testing.expect(t, os.write_entire_file(
"/tmp/brolang-test-poisoned-declarations/main.bro",
transmute([]byte)text,
))
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load(directory, &sources, &diagnostics, &symbols)
defer ast.destroy_module(&module)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 2)
unknown_missing := false
unknown_absent := false
for diagnostic in diagnostics.items {
unknown_missing = unknown_missing || diagnostic.message == "unknown symbol 'missing'"
unknown_absent = unknown_absent || diagnostic.message == "unknown symbol 'absent'"
testing.expect(t, !strings.contains(diagnostic.message, "could not resolve"))
testing.expect(t, !strings.contains(diagnostic.message, "could not infer"))
}
testing.expect(t, unknown_missing)
testing.expect(t, unknown_absent)
}
named_record_field_type :: proc(
module: ^hir.Module,
symbols: ^symbol.Table,