refactor: Moved diagnostic code into its own package.

This commit is contained in:
Spencer Brower
2026-08-05 18:13:16 -04:00
parent 307fb9ddc0
commit fb765fc83b
11 changed files with 509 additions and 416 deletions
+37 -242
View File
@@ -1,61 +1,6 @@
package mustache
import "core:fmt"
import "core:os"
import "core:strings"
import "core:terminal/ansi"
import "core:unicode/utf8"
// line_col returns the 1-indexed line and column for a byte offset in source.
// Newlines ('\n') separate lines; '\r' is treated as part of '\r\n'. Column is
// counted in bytes from the start of the line.
line_col :: proc(source: string, pos_in: int) -> (line: int, col: int) {
pos := pos_in
if pos < 0 {
return 1, 1
}
if pos > len(source) {
pos = len(source)
}
line = 1
col = 1
for i := 0; i < pos; i += 1 {
if source[i] == '\n' {
line += 1
col = 1
} else {
col += 1
}
}
return
}
// line_text returns the Nth (1-indexed) line of source, without the trailing
// newline. Returns "" if line is out of range.
line_text :: proc(source: string, line: int) -> string {
if line < 1 {
return ""
}
current := 1
start := 0
for i := 0; i < len(source); i += 1 {
if current == line {
end := i
for end < len(source) && source[end] != '\n' {
end += 1
}
return source[start:end]
}
if source[i] == '\n' {
current += 1
start = i + 1
}
}
if current == line {
return source[start:]
}
return ""
}
import diags "../diagnostics"
// context_extent returns the byte offset of the start of the line containing
// pos, plus the byte offsets of the start and end of the mustache tag at pos.
@@ -115,68 +60,11 @@ context_extent :: proc(
return line_start, pos, pos + 1
}
// should_colorize returns true if stderr is a TTY and color output is wanted.
should_colorize :: proc() -> bool {
return os.is_tty(os.stderr)
}
// count_lines returns the number of '\n'-separated lines in source.
// A trailing newline does not add an extra line.
count_lines :: proc(source: string) -> int {
if len(source) == 0 {
return 1
}
n := 1
for c in source {
if c == '\n' {
n += 1
}
}
// Drop phantom last line if source ends with '\n'.
if len(source) > 0 && source[len(source) - 1] == '\n' {
n -= 1
}
return n
}
// digit_count returns the number of decimal digits in n (min 1).
digit_count :: proc(n: int) -> int {
if n <= 0 {
return 1
}
c := 0
x := n
for x > 0 {
c += 1
x /= 10
}
return c
}
// display_width returns the number of terminal cells `s` occupies.
// For ASCII this is byte length; for UTF-8 we count runes (combining
// marks and wide CJK chars are still approximate).
display_width :: proc(s: string) -> int {
return utf8.rune_count_in_string(s)
}
// format_error produces a rust-style multi-line diagnostic string.
//
// <msg>
// --> <path>:<line>:<col>
// |
// N | <source line N-2>
// N | <source line N-1>
// N | <source line N — the error line>
// | ^^^^^^^^^^^ <hint>
// N | <source line N+1>
// N | <source line N+2>
// |
//
// `context_before`/`context_after` lines of context are shown around the
// error line. Line numbers are right-aligned to the width of the largest
// line number shown.
format_error :: proc(
// format_tag_error is a mustache-aware wrapper around diags.format_error.
// It uses context_extent to compute the caret underline for `{{ }}` tags,
// falling back to a single-rune underline when pos is not inside a tag.
// When `span > 0`, the caret extends from pos to pos + span.
format_tag_error :: proc(
path: string,
source: string,
pos: int,
@@ -187,135 +75,25 @@ format_error :: proc(
colorize: bool = false,
span: int = 0,
) -> string {
line, col := line_col(source, pos)
total_lines := count_lines(source)
start_line := line - context_before
if start_line < 1 {
start_line = 1
}
end_line := line + context_after
if end_line > total_lines {
end_line = total_lines
}
// Width of the line-number column (right-align).
width := digit_count(end_line)
if width < 1 {
width = 1
}
sb := strings.builder_make(context.temp_allocator)
defer strings.builder_destroy(&sb)
color := colorize
red, faint, reset := "", "", ""
if color {
red = ansi.CSI + ansi.FG_RED + ansi.SGR
faint = ansi.CSI + ansi.FAINT + ansi.SGR
reset = ansi.CSI + ansi.RESET + ansi.SGR
}
// Header line: message.
strings.write_string(&sb, msg)
strings.write_byte(&sb, '\n')
// Location line: " --> path:line:col" (width spaces + arrow).
strings.write_string(&sb, faint)
for _ in 0 ..< width {
strings.write_byte(&sb, ' ')
}
strings.write_string(&sb, "--> ")
strings.write_string(&sb, reset)
fmt.sbprintf(&sb, "%s:%d:%d\n", path, line, col)
// Top gutter line.
write_gutter(&sb, width, faint, reset)
// Caret extent for the error line.
line_start, token_start, token_end := context_extent(source, pos)
caret_start := pos
caret_end := pos + 1
if span > 0 {
token_start = pos
token_end = pos + span
caret_end = pos + span
} else {
_, ts, te := context_extent(source, pos)
caret_start = ts
caret_end = te
}
caret_start_col := token_start - line_start + 1
caret_end_col := token_end - line_start + 1
if caret_end_col <= caret_start_col {
caret_end_col = caret_start_col + 1
}
// Context lines.
for n in start_line ..= end_line {
// Line number (right-aligned, faint).
num_str := fmt.tprintf("%d", n)
strings.write_string(&sb, faint)
for _ in 0 ..< width - len(num_str) {
strings.write_byte(&sb, ' ')
}
strings.write_string(&sb, num_str)
strings.write_string(&sb, " | ")
strings.write_string(&sb, reset)
strings.write_string(&sb, line_text(source, n))
strings.write_byte(&sb, '\n')
// After the error line, emit the caret row.
if n == line {
strings.write_string(&sb, faint)
for _ in 0 ..< width + 1 {
strings.write_byte(&sb, ' ')
}
strings.write_string(&sb, "| ")
strings.write_string(&sb, reset)
for _ in 1 ..< caret_start_col {
strings.write_byte(&sb, ' ')
}
if color {
strings.write_string(&sb, red)
}
for _ in 0 ..< caret_end_col - caret_start_col {
strings.write_byte(&sb, '^')
}
if color {
strings.write_string(&sb, reset)
}
if hint != "" {
strings.write_byte(&sb, ' ')
if color {
strings.write_string(&sb, faint)
}
strings.write_string(&sb, hint)
if color {
strings.write_string(&sb, reset)
}
}
strings.write_byte(&sb, '\n')
}
}
// Trailing gutter line for visual closure.
write_gutter(&sb, width, faint, reset)
return strings.to_string(sb)
}
// write_gutter emits a faint pipe-only gutter line: `<width+1 spaces> |`.
write_gutter :: proc(sb: ^strings.Builder, width: int, faint: string, reset: string) {
strings.write_string(sb, faint)
for _ in 0 ..< width + 1 {
strings.write_byte(sb, ' ')
}
strings.write_string(sb, "|")
strings.write_string(sb, reset)
strings.write_byte(sb, '\n')
return diags.format_error(
path, source, pos, msg, hint,
context_before, context_after,
colorize,
caret_start, caret_end,
)
}
// format_render_error produces a diagnostic for an Error value using the
// template's path and source for context. Returns "" for nil errors.
// If the error carries its own source/path (from tag_error), those are used
// instead of the passed-in template — this ensures errors inside partials
// point at the correct file.
format_render_error :: proc(err: Error, tmpl: Template, colorize: bool = false) -> string {
if err == nil {
return ""
@@ -326,5 +104,22 @@ format_render_error :: proc(err: Error, tmpl: Template, colorize: bool = false)
if path == "" {
path = "<input>"
}
return format_error(path, source, b.pos, b.msg, hint = b.hint, colorize = colorize, span = b.span)
caret_start := b.pos
caret_end := b.pos + 1
if b.span > 0 {
caret_end = b.pos + b.span
} else {
_, ts, te := context_extent(source, b.pos)
caret_start = ts
caret_end = te
}
return diags.format_error(
path, source, b.pos, b.msg,
hint = b.hint,
colorize = colorize,
caret_start = caret_start,
caret_end = caret_end,
)
}
+19 -101
View File
@@ -6,95 +6,13 @@ import "core:strings"
import "core:testing"
// ---------------------------------------------------------------------------
// line_col / line_text / count_lines / digit_count — primitive helpers
// ---------------------------------------------------------------------------
@(test)
test_line_col_basic :: proc(t: ^testing.T) {
src := "abc\ndef\nghi"
cases := [?]struct {
pos: int,
line: int,
col: int,
}{{0, 1, 1}, {2, 1, 3}, {3, 1, 4}, {4, 2, 1}, {6, 2, 3}, {7, 2, 4}, {8, 3, 1}}
for c in cases {
l, col := line_col(src, c.pos)
testing.expect(t, l == c.line, fmt.tprintf("pos %d: line %d, want %d", c.pos, l, c.line))
testing.expect(t, col == c.col, fmt.tprintf("pos %d: col %d, want %d", c.pos, col, c.col))
}
}
@(test)
test_line_col_empty :: proc(t: ^testing.T) {
l, col := line_col("", 0)
testing.expect_value(t, l, 1)
testing.expect_value(t, col, 1)
}
@(test)
test_line_col_negative :: proc(t: ^testing.T) {
l, col := line_col("abc", -1)
testing.expect_value(t, l, 1)
testing.expect_value(t, col, 1)
}
@(test)
test_line_col_past_end :: proc(t: ^testing.T) {
l, col := line_col("abc", 100)
testing.expect_value(t, l, 1)
testing.expect_value(t, col, 4)
}
@(test)
test_line_text_first :: proc(t: ^testing.T) {
src := "first\nsecond\nthird"
testing.expect_value(t, line_text(src, 1), "first")
testing.expect_value(t, line_text(src, 2), "second")
testing.expect_value(t, line_text(src, 3), "third")
}
@(test)
test_line_text_trailing_newline :: proc(t: ^testing.T) {
src := "first\nsecond\n"
testing.expect_value(t, line_text(src, 1), "first")
testing.expect_value(t, line_text(src, 2), "second")
testing.expect_value(t, line_text(src, 3), "")
}
@(test)
test_line_text_out_of_range :: proc(t: ^testing.T) {
testing.expect_value(t, line_text("abc", 5), "")
testing.expect_value(t, line_text("abc", 0), "")
}
@(test)
test_count_lines :: proc(t: ^testing.T) {
testing.expect_value(t, count_lines(""), 1)
testing.expect_value(t, count_lines("abc"), 1)
testing.expect_value(t, count_lines("a\nb"), 2)
testing.expect_value(t, count_lines("a\nb\n"), 2)
testing.expect_value(t, count_lines("a\nb\nc"), 3)
}
@(test)
test_digit_count :: proc(t: ^testing.T) {
testing.expect_value(t, digit_count(0), 1)
testing.expect_value(t, digit_count(1), 1)
testing.expect_value(t, digit_count(9), 1)
testing.expect_value(t, digit_count(10), 2)
testing.expect_value(t, digit_count(99), 2)
testing.expect_value(t, digit_count(100), 3)
testing.expect_value(t, digit_count(-5), 1)
}
// ---------------------------------------------------------------------------
// format_error — golden output tests
// format_tag_error — golden output tests
// ---------------------------------------------------------------------------
@(test)
test_format_error_basic :: proc(t: ^testing.T) {
src := "line 1\nline 2\n{{bad}}\nline 4\nline 5"
out := format_error("p.html", src, 14, "unknown key 'bad'", "", colorize = false)
out := format_tag_error("p.html", src, 14, "unknown key 'bad'", "", colorize = false)
expected := `unknown key 'bad'
--> p.html:3:1
|
@@ -112,7 +30,7 @@ test_format_error_basic :: proc(t: ^testing.T) {
@(test)
test_format_error_with_hint :: proc(t: ^testing.T) {
src := "line 1\nline 2\n{{titel}}\nline 4\nline 5"
out := format_error(
out := format_tag_error(
"post.html",
src,
14,
@@ -137,7 +55,7 @@ test_format_error_with_hint :: proc(t: ^testing.T) {
@(test)
test_format_error_no_hint_omits_trailing_space :: proc(t: ^testing.T) {
src := "{{bad}}"
out := format_error("p.html", src, 0, "msg", "", colorize = false)
out := format_tag_error("p.html", src, 0, "msg", "", colorize = false)
// Caret line ends immediately after the carets — no trailing space.
testing.expect(t, strings.contains(out, "^^^^^^^\n"), out)
testing.expect(t, !strings.contains(out, "^^^^^^^ \n"), out)
@@ -150,7 +68,7 @@ test_format_error_no_hint_omits_trailing_space :: proc(t: ^testing.T) {
@(test)
test_format_error_first_line_only_after_context :: proc(t: ^testing.T) {
src := "{{bad}}\nline 2\nline 3\nline 4\nline 5"
out := format_error("p.html", src, 0, "msg", "", colorize = false)
out := format_tag_error("p.html", src, 0, "msg", "", colorize = false)
expected := `msg
--> p.html:1:1
|
@@ -166,7 +84,7 @@ test_format_error_first_line_only_after_context :: proc(t: ^testing.T) {
@(test)
test_format_error_last_line_only_before_context :: proc(t: ^testing.T) {
src := "line 1\nline 2\nline 3\nline 4\n{{bad}}"
out := format_error("p.html", src, 28, "msg", "", colorize = false)
out := format_tag_error("p.html", src, 28, "msg", "", colorize = false)
expected := `msg
--> p.html:5:1
|
@@ -182,7 +100,7 @@ test_format_error_last_line_only_before_context :: proc(t: ^testing.T) {
@(test)
test_format_error_short_source_clamped :: proc(t: ^testing.T) {
src := "x\n{{bad}}\ny"
out := format_error("p.html", src, 2, "msg", "", colorize = false)
out := format_tag_error("p.html", src, 2, "msg", "", colorize = false)
expected := `msg
--> p.html:2:1
|
@@ -198,7 +116,7 @@ test_format_error_short_source_clamped :: proc(t: ^testing.T) {
@(test)
test_format_error_single_line_source :: proc(t: ^testing.T) {
src := "{{bad}}"
out := format_error("p.html", src, 0, "msg", "", colorize = false)
out := format_tag_error("p.html", src, 0, "msg", "", colorize = false)
expected := `msg
--> p.html:1:1
|
@@ -214,7 +132,7 @@ test_format_error_two_digit_line_numbers :: proc(t: ^testing.T) {
// 12-line source; error on line 9. end_line=11 → width=2.
src := "l01\nl02\nl03\nl04\nl05\nl06\nl07\nl08\n{{bad}}\nl10\nl11\nl12"
// Position of `{{bad}}`: 8 lines of "l0N\n" = 8*4 = 32 bytes.
out := format_error("p.html", src, 32, "msg", "", colorize = false)
out := format_tag_error("p.html", src, 32, "msg", "", colorize = false)
testing.expect(t, strings.contains(out, " --> p.html:9:1\n"), out)
testing.expect(t, strings.contains(out, " |\n"), out)
testing.expect(t, strings.contains(out, " 7 | l07\n"), out)
@@ -241,7 +159,7 @@ test_format_error_three_digit_line_numbers :: proc(t: ^testing.T) {
pos += len(parts[i - 1]) + 1
}
out := format_error("p.html", src, pos, "msg", "", colorize = false)
out := format_tag_error("p.html", src, pos, "msg", "", colorize = false)
testing.expect(t, strings.contains(out, " --> p.html:100:1\n"), out)
testing.expect(t, strings.contains(out, " |\n"), out)
testing.expect(t, strings.contains(out, " 98 | l098\n"), out)
@@ -256,7 +174,7 @@ test_format_error_three_digit_line_numbers :: proc(t: ^testing.T) {
@(test)
test_caret_at_column_1 :: proc(t: ^testing.T) {
src := "{{bad}} at start"
out := format_error("p.html", src, 0, "msg", "", colorize = false)
out := format_tag_error("p.html", src, 0, "msg", "", colorize = false)
// Caret line should start with "^" right after "| " (no leading spaces).
testing.expect(t, strings.contains(out, " | ^^^^^^^\n"), out)
}
@@ -265,7 +183,7 @@ test_caret_at_column_1 :: proc(t: ^testing.T) {
test_caret_at_column_N :: proc(t: ^testing.T) {
src := " {{bad}}"
// pos=4 is the first '{'. Line 1, col 5.
out := format_error("p.html", src, 4, "msg", "", colorize = false)
out := format_tag_error("p.html", src, 4, "msg", "", colorize = false)
// 4 leading spaces, then 7 carets.
testing.expect(t, strings.contains(out, " | ^^^^^^^\n"), out)
}
@@ -273,7 +191,7 @@ test_caret_at_column_N :: proc(t: ^testing.T) {
@(test)
test_caret_width_matches_token :: proc(t: ^testing.T) {
src := "{{x}}"
out := format_error("p.html", src, 0, "msg", "", colorize = false)
out := format_tag_error("p.html", src, 0, "msg", "", colorize = false)
// {{x}} is 5 chars wide.
testing.expect(t, strings.contains(out, " | ^^^^^\n"), out)
}
@@ -285,7 +203,7 @@ test_caret_width_matches_token :: proc(t: ^testing.T) {
@(test)
test_context_before_zero :: proc(t: ^testing.T) {
src := "l1\nl2\nl3\n{{bad}}\nl5\nl6"
out := format_error(
out := format_tag_error(
"p.html",
src,
9,
@@ -309,7 +227,7 @@ test_context_before_zero :: proc(t: ^testing.T) {
@(test)
test_context_after_zero :: proc(t: ^testing.T) {
src := "l1\nl2\nl3\n{{bad}}\nl5\nl6"
out := format_error(
out := format_tag_error(
"p.html",
src,
9,
@@ -333,7 +251,7 @@ test_context_after_zero :: proc(t: ^testing.T) {
@(test)
test_context_both_zero :: proc(t: ^testing.T) {
src := "l1\nl2\nl3\n{{bad}}\nl5\nl6"
out := format_error(
out := format_tag_error(
"p.html",
src,
9,
@@ -360,7 +278,7 @@ test_context_both_zero :: proc(t: ^testing.T) {
@(test)
test_gutter_pipes_align_with_source_pipe :: proc(t: ^testing.T) {
src := "l1\n{{bad}}\nl3"
out := format_error("p.html", src, 3, "msg", "", colorize = false)
out := format_tag_error("p.html", src, 3, "msg", "", colorize = false)
// All "|" characters should appear at the same column.
// For width=1: source line is "N | ...", so "|" at col 2.
// Empty gutter is " |" (width+1 spaces + "|"), so "|" at col 2.
@@ -382,7 +300,7 @@ test_gutter_pipes_align_with_source_pipe :: proc(t: ^testing.T) {
@(test)
test_arrow_points_at_pipe :: proc(t: ^testing.T) {
src := "{{bad}}"
out := format_error("p.html", src, 0, "msg", "", colorize = false)
out := format_tag_error("p.html", src, 0, "msg", "", colorize = false)
// For width=1: arrow line is " --> ..." so ">" at col 3.
// Pipe lines are " |" so "|" at col 2.
lines := strings.split(out, "\n", context.temp_allocator)
@@ -424,7 +342,7 @@ test_format_render_error_dispatch :: proc(t: ^testing.T) {
}
b := body(parse_err)
out := format_error("test.html", src, b.pos, b.msg, colorize = false)
out := format_tag_error("test.html", src, b.pos, b.msg, colorize = false)
testing.expect(t, strings.contains(out, "unclosed section"), out)
testing.expect(t, strings.contains(out, "test.html:"), out)
}
+10 -8
View File
@@ -5,6 +5,8 @@ import "core:fmt"
import "core:log"
import "core:strings"
import diags "../diagnostics"
// A hypothetical maximum context depth. Trying to pass more than this many items
// to render(tpl, data), or nesting templates further than this depth would be
// an error.
@@ -842,7 +844,7 @@ warn_unknown_key :: proc(current: Template, ctx: []any, node: Node) {
hint := ""
if len(available) > 0 {
suggestion := suggest_correction(available, missing)
suggestion := diags.suggest_correction(available, missing)
if suggestion != "" {
hint = fmt.tprintf("did you mean '%s'?", suggestion)
}
@@ -852,7 +854,7 @@ warn_unknown_key :: proc(current: Template, ctx: []any, node: Node) {
if path == "" {
path = "<input>"
}
diag := format_error(path, current.source, node.pos, msg, hint, colorize = should_colorize())
diag := format_tag_error(path, current.source, node.pos, msg, hint, colorize = diags.should_colorize())
log.warnf("%s", diag)
}
@@ -866,7 +868,7 @@ warn_missing_partial :: proc(
) {
hint := ""
available := collect_partial_names(partials)
suggestion := suggest_correction(available, name)
suggestion := diags.suggest_correction(available, name)
if suggestion != "" {
hint = fmt.tprintf("did you mean '%s'?", suggestion)
}
@@ -875,7 +877,7 @@ warn_missing_partial :: proc(
if path == "" {
path = "<input>"
}
diag := format_error(path, current.source, node.pos, msg, hint, colorize = should_colorize())
diag := format_tag_error(path, current.source, node.pos, msg, hint, colorize = diags.should_colorize())
log.warnf("%s", diag)
}
@@ -911,7 +913,7 @@ warn_unmatched_block_overrides :: proc(
}
hint := ""
suggestion := suggest_correction(available, child.key)
suggestion := diags.suggest_correction(available, child.key)
if suggestion != "" {
hint = fmt.tprintf("did you mean '%s'?", suggestion)
}
@@ -924,13 +926,13 @@ warn_unmatched_block_overrides :: proc(
if path == "" {
path = "<input>"
}
diag := format_error(
diag := format_tag_error(
path,
current.source,
child.pos,
msg,
hint,
colorize = should_colorize(),
colorize = diags.should_colorize(),
)
log.warnf("%s", diag)
}
@@ -954,6 +956,6 @@ warn_context_depth :: proc(current: Template, node: Node) {
if path == "" {
path = "<input>"
}
diag := format_error(path, current.source, node.pos, msg, "", colorize = should_colorize())
diag := format_tag_error(path, current.source, node.pos, msg, "", colorize = diags.should_colorize())
log.warnf("%s", diag)
}
+3 -1
View File
@@ -6,6 +6,8 @@ import "core:log"
import "core:reflect"
import "core:strings"
import "core:time"
import diags "../diagnostics"
import "core:time/datetime"
// MAX_PIPES was chosen arbitrarily. It holds no performance or logical
@@ -260,7 +262,7 @@ apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int, ctx: []any) ->
op, ok := pipe_op_from_string(filter.op)
if !ok {
hint := ""
suggestion := suggest_correction(pipe_op_candidates(), filter.op)
suggestion := diags.suggest_correction(pipe_op_candidates(), filter.op)
if suggestion != "" {
hint = fmt.tprintf("did you mean '%s'?", suggestion)
}
+3 -25
View File
@@ -3,6 +3,8 @@ package mustache
import "base:runtime"
import "core:strings"
import diags "../diagnostics"
// collect_struct_keys enumerates the visible field names of a struct value,
// including fields promoted via `using`-embedded structs.
collect_struct_keys :: proc(val: any, allocator := context.temp_allocator) -> []string {
@@ -130,7 +132,7 @@ validate_key_path :: proc(
continue
}
available := collect_map_keys(current, allocator)
if suggest_correction(available, parts[i]) != "" {
if diags.suggest_correction(available, parts[i]) != "" {
return false, parts[i], available
}
return true, "", nil
@@ -155,30 +157,6 @@ validate_key_path :: proc(
return true, "", nil
}
// suggest_correction returns the closest match from `available` to `missing`
// using Levenshtein distance, or "" if no good match exists. The threshold
// scales with the length of the missing key.
suggest_correction :: proc(available: []string, missing: string) -> string {
if len(available) == 0 || len(missing) == 0 {
return ""
}
threshold := max(2, len(missing) / 3)
best: string
best_dist := threshold + 1
for candidate in available {
if abs(len(candidate) - len(missing)) > threshold {
continue
}
d := strings.levenshtein_distance(missing, candidate)
if d <= threshold && d < best_dist {
best = candidate
best_dist = d
}
}
return best
}
// collect_partial_names enumerates the keys of the partials map.
collect_partial_names :: proc(
partials: map[string]Template,
+3 -25
View File
@@ -6,6 +6,8 @@ import "core:encoding/json"
import "core:fmt"
import "core:testing"
import diags "../diagnostics"
Inner :: struct {
foo: string,
bar: int,
@@ -153,30 +155,6 @@ test_validate_map_path_silent :: proc(t: ^testing.T) {
testing.expect_value(t, ok, true)
}
@(test)
test_suggest_correction_exact :: proc(t: ^testing.T) {
available := []string{"title", "page.title", "body"}
testing.expect_value(t, suggest_correction(available, "page_titel"), "page.title")
}
@(test)
test_suggest_correction_close :: proc(t: ^testing.T) {
available := []string{"title", "body", "now"}
testing.expect_value(t, suggest_correction(available, "titel"), "title")
}
@(test)
test_suggest_correction_no_match :: proc(t: ^testing.T) {
available := []string{"completely_different", "unrelated"}
testing.expect_value(t, suggest_correction(available, "page_titel"), "")
}
@(test)
test_suggest_correction_empty :: proc(t: ^testing.T) {
testing.expect_value(t, suggest_correction([]string{}, "anything"), "")
testing.expect_value(t, suggest_correction([]string{"a"}, ""), "")
}
@(test)
test_warn_no_false_positive_for_valid_keys :: proc(t: ^testing.T) {
Data :: struct {
@@ -244,7 +222,7 @@ test_validate_key_path_map_typo :: proc(t: ^testing.T) {
ok, missing, available := validate_key_path(ctx[:], "params.authr")
testing.expect(t, !ok, "typo of map key should not be ok")
testing.expect_value(t, missing, "authr")
suggestion := suggest_correction(available, "authr")
suggestion := diags.suggest_correction(available, "authr")
testing.expect_value(t, suggestion, "author")
}