From bd964b8dd9c5223bff2151e7c8784824a545eb2a Mon Sep 17 00:00:00 2001 From: Nick Gasson Date: Sun, 24 Jul 2022 14:52:55 +0100 Subject: [PATCH] Make suggestions for typos --- src/diag.c | 10 +++++--- src/ident.c | 33 ++++++++++++++++++++++++++ src/ident.h | 3 +++ src/names.c | 58 ++++++++++++++++++++++++++++++++++++++++----- test/parse/typo.vhd | 30 +++++++++++++++++++++++ test/test_ident.c | 42 ++++++++++++++++++++++++++++++++ test/test_parse.c | 54 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 test/parse/typo.vhd diff --git a/src/diag.c b/src/diag.c index b826d270..31afadd2 100644 --- a/src/diag.c +++ b/src/diag.c @@ -880,8 +880,9 @@ const char *diag_get_text(diag_t *d) const char *diag_get_hint(diag_t *d, int nth) { - assert(nth + 1 < d->hints.count); - return d->hints.items[nth + 1].text; + if (d->hints.items[0].text == NULL) nth++; + assert(nth < d->hints.count); + return d->hints.items[nth].text; } const char *diag_get_trace(diag_t *d, int nth) @@ -900,7 +901,10 @@ const loc_t *diag_get_loc(diag_t *d) int diag_hints(diag_t *d) { - return d->hints.count - 1; + if (d->hints.items[0].text == NULL) + return d->hints.count - 1; + else + return d->hints.count; } int diag_traces(diag_t *d) diff --git a/src/ident.c b/src/ident.c index 30729de0..12e18320 100644 --- a/src/ident.c +++ b/src/ident.c @@ -604,3 +604,36 @@ ident_t ident_walk_selected(ident_t *i) return result; } + +int ident_distance(ident_t a, ident_t b) +{ + const int n = ident_len(b); + const int m = ident_len(a); + + char s[m + 1], t[n + 1]; + istr_r(a, s, m + 1); + istr_r(b, t, n + 1); + + int mem[2 * (n + 1)], *v0 = mem, *v1 = mem + n + 1; + + for (int i = 0; i <= n; i++) + v0[i] = i; + + for (int i = 0; i < m; i++) { + v1[0] = i + 1; + + for (int j = 0; j < n; j++) { + const int dc = v0[j + 1] + 1; + const int ic = v1[j] + 1; + const int sc = (s[i] == t[j] ? v0[j] : v0[j] + 1); + + v1[j + 1] = MIN(dc, MIN(ic, sc)); + } + + int *tmp = v0; + v0 = v1; + v1 = tmp; + } + + return v0[n]; +} diff --git a/src/ident.h b/src/ident.h index 57f97865..5279a76b 100644 --- a/src/ident.h +++ b/src/ident.h @@ -86,6 +86,9 @@ const char *istr(ident_t ident); // As above but write into a user supplied buffer. void istr_r(ident_t ident, char *buf, size_t sz); +// Compute Levenshtein distance between two identifiers +int ident_distance(ident_t a, ident_t b); + ident_wr_ctx_t ident_write_begin(fbuf_t *f); void ident_write(ident_t ident, ident_wr_ctx_t ctx); void ident_write_end(ident_wr_ctx_t ctx); diff --git a/src/names.c b/src/names.c index d5f3734a..578cda95 100644 --- a/src/names.c +++ b/src/names.c @@ -850,6 +850,7 @@ static symbol_t *make_visible(scope_t *s, ident_t name, tree_t decl, static void merge_symbol(scope_t *s, const symbol_t *src) { symbol_t *dst = local_symbol_for(s, src->name); + dst->mask |= src->mask; const bool was_fresh = (dst->ndecls == 0); @@ -1261,6 +1262,33 @@ static const symbol_t *iterate_symbol_for(nametab_t *tab, ident_t name) } } +static void hint_for_typo(nametab_t *tab, diag_t *d, ident_t name, + name_mask_t filter) +{ + if (ident_runtil(name, '.') != name) + return; // Ignore selected names + + const symbol_t *best = NULL; + int bestd = INT_MAX; + + for (scope_t *s = tab->top_scope; s != NULL; s = s->parent) { + for (sym_chunk_t *chunk = &(s->symbols); chunk; chunk = chunk->chain) { + for (int i = 0; i < chunk->count; i++) { + if (chunk->symbols[i].mask & filter) { + const int d = ident_distance(chunk->symbols[i].name, name); + if (d < bestd) { + best = &(chunk->symbols[i]); + bestd = d; + } + } + } + } + } + + if (bestd <= (ident_len(name) <= 4 ? 2 : 3)) + diag_hint(d, diag_get_loc(d), "did you mean %s?", istr(best->name)); +} + tree_t resolve_name(nametab_t *tab, const loc_t *loc, ident_t name) { const symbol_t *sym = iterate_symbol_for(tab, name); @@ -1308,8 +1336,12 @@ tree_t resolve_name(nametab_t *tab, const loc_t *loc, ident_t name) } else if (tab->top_scope->formal_kind != F_NONE) ; // Was earlier error - else if (tab->top_scope->overload == NULL) - error_at(loc, "no visible declaration for %s", istr(name)); + else if (tab->top_scope->overload == NULL) { + diag_t *d = diag_new(DIAG_ERROR, loc); + diag_printf(d, "no visible declaration for %s", istr(name)); + hint_for_typo(tab, d, name, N_OBJECT | N_TYPE); + diag_emit(d); + } // Suppress further errors for this name local_symbol_for(tab->top_scope, name)->mask |= N_ERROR; @@ -2006,18 +2038,20 @@ static void begin_overload_resolution(overload_t *o) if (o->initial == 0 && !o->error) { diag_t *d = diag_new(DIAG_ERROR, tree_loc(o->tree)); diag_printf(d, "no visible subprogram declaration for %s", istr(o->name)); + hint_for_typo(o->nametab, d, o->name, N_SUBPROGRAM); if (sym != NULL) { + const bool hinted = diag_hints(d) > 0; for (int i = 0; i < sym->ndecls; i++) { const decl_t *dd = get_decl(sym, i); if ((dd->mask & N_SUBPROGRAM) && dd->visibility == HIDDEN) diag_hint(d, tree_loc(dd->tree), "subprogram %s is hidden", type_pp(tree_type(dd->tree))); } - } - if (diag_hints(d) > 0) - diag_hint(d, tree_loc(o->tree), "%s called here", istr(o->name)); + if (!hinted) + diag_hint(d, tree_loc(o->tree), "%s called here", istr(o->name)); + } diag_emit(d); o->error = true; @@ -3030,16 +3064,28 @@ static type_t solve_record_ref(nametab_t *tab, tree_t rref) diag_printf(d, "record type %s has no field named %s", type_pp(value_type), istr(tree_ident(rref))); + ident_t best = NULL; + int bestd = INT_MAX; + LOCAL_TEXT_BUF tb = tb_new(); int nfields = type_fields(value_type), i; tb_printf(tb, "type %s has fields ", type_pp(value_type)); for (i = 0; i < nfields; i++) { + ident_t id = tree_ident(type_field(value_type, i)); + const int d = ident_distance(id, tree_ident(rref)); + if (d < bestd) { + best = id; + bestd = d; + } if (i > 0 && i == nfields - 1) tb_cat(tb, ", and "); else if (i > 0) tb_cat(tb, ", "); - tb_istr(tb, tree_ident(type_field(value_type, i))); + tb_istr(tb, id); } diag_hint(d, NULL, "%s", tb_get(tb)); + if (bestd <= 3) + diag_hint(d, tree_loc(rref), "did you mean %s?", istr(best)); + diag_emit(d); type = type_new(T_NONE); } diff --git a/test/parse/typo.vhd b/test/parse/typo.vhd new file mode 100644 index 00000000..75c028f1 --- /dev/null +++ b/test/parse/typo.vhd @@ -0,0 +1,30 @@ +entity typo is +end entity; + +architecture test of typo is + signal areset, reset : bit; + constant k : bolean; -- Error + + type rec is record + foo, bar : integer; + end record; + +begin + assert rset; -- Error + + p1: process is + function myfunc(x : integer) return integer is + begin + return x * 2; + end function; + begin + assert noew = 45 ns; -- Error + assert my_func(4) = 8; -- Error + end process; + + p2: process is + variable r : rec; + begin + r.frodo := 1; + end process; +end architecture; diff --git a/test/test_ident.c b/test/test_ident.c index 7d6fd209..b1144b5c 100644 --- a/test/test_ident.c +++ b/test/test_ident.c @@ -365,6 +365,47 @@ START_TEST(test_istr_r) } END_TEST +START_TEST(test_distance) +{ + const char *words[17] = { + "previous", "clam", "loud", "sore", "striped", "healthy", + "automatic", "spy", "surround", "trade", "flowers" "nifty", + "chickens", "beef", "nutty", "kindly", "kitten", "sitting", + }; + + ident_t ids[17]; + for (int i = 0; i < 17; i++) + ids[i] = ident_new(words[i]); + + const int expect[17][17] = { + { 0, 8, 6, 7, 7, 7, 9, 8, 7, 7, 10, 7, 7, 8, 8, 8, 7, }, + { 8, 0, 4, 4, 7, 6, 8, 4, 8, 4, 11, 7, 4, 5, 6, 6, 7, }, + { 6, 4, 0, 3, 6, 6, 8, 4, 5, 4, 10, 8, 4, 5, 5, 6, 7, }, + { 7, 4, 3, 0, 4, 7, 8, 3, 6, 4, 10, 7, 4, 5, 6, 5, 6, }, + { 7, 7, 6, 4, 0, 7, 8, 5, 5, 4, 10, 7, 6, 7, 7, 6, 5, }, + { 7, 6, 6, 7, 7, 0, 8, 6, 8, 6, 10, 7, 6, 5, 6, 6, 7, }, + { 9, 8, 8, 8, 8, 8, 0, 9, 8, 7, 11, 9, 9, 6, 9, 7, 7, }, + { 8, 4, 4, 3, 5, 6, 9, 0, 7, 5, 10, 8, 4, 4, 5, 6, 6, }, + { 7, 8, 5, 6, 5, 8, 8, 7, 0, 7, 11, 7, 8, 7, 8, 7, 6, }, + { 7, 4, 4, 4, 4, 6, 7, 5, 7, 0, 11, 7, 5, 5, 5, 5, 6, }, + { 10, 11, 10, 10, 10, 10, 11, 10, 11, 11, 0, 11, 10, 9, 10, 10, 11, }, + { 7, 7, 8, 7, 7, 7, 9, 8, 7, 7, 11, 0, 7, 8, 7, 5, 6, }, + { 7, 4, 4, 4, 6, 6, 9, 4, 8, 5, 10, 7, 0, 5, 6, 5, 7, }, + { 8, 5, 5, 5, 7, 5, 6, 4, 7, 5, 9, 8, 5, 0, 5, 4, 5, }, + { 8, 6, 5, 6, 7, 6, 9, 5, 8, 5, 10, 7, 6, 5, 0, 4, 6, }, + { 8, 6, 6, 5, 6, 6, 7, 6, 7, 5, 10, 5, 5, 4, 4, 0, 3, }, + { 7, 7, 7, 6, 5, 7, 7, 6, 6, 6, 11, 6, 7, 5, 6, 3, 0, }, + }; + + for (int i = 0; i < 17; i++) { + for (int j = 0; j < 17; j++) { + const int d = ident_distance(ids[i], ids[j]); + ck_assert_int_eq(d, expect[i][j]); + } + } +} +END_TEST + Suite *get_ident_tests(void) { Suite *s = suite_create("ident"); @@ -392,6 +433,7 @@ Suite *get_ident_tests(void) tcase_add_test(tc_core, test_walk_selected); tcase_add_test(tc_core, test_starts_with); tcase_add_test(tc_core, test_istr_r); + tcase_add_test(tc_core, test_distance); suite_add_tcase(s, tc_core); return s; diff --git a/test/test_parse.c b/test/test_parse.c index fa84b9ca..fa28da2e 100644 --- a/test/test_parse.c +++ b/test/test_parse.c @@ -4851,6 +4851,59 @@ START_TEST(test_issue479) } END_TEST +static void typo_diag_fn(diag_t *d) +{ + static int state = 0; + switch (state++) { + case 0: + ck_assert_str_eq(diag_get_text(d), "no visible declaration for BOLEAN"); + ck_assert_str_eq(diag_get_hint(d, 0), "did you mean BOOLEAN?"); + break; + case 1: + ck_assert_str_eq(diag_get_text(d), "no visible declaration for RSET"); + ck_assert_str_eq(diag_get_hint(d, 0), "did you mean RESET?"); + break; + case 2: + ck_assert_str_eq(diag_get_text(d), "no visible declaration for NOEW"); + ck_assert_int_eq(diag_hints(d), 0); + break; + case 3: + ck_assert_str_eq(diag_get_text(d), + "no visible subprogram declaration for MY_FUNC"); + ck_assert_str_eq(diag_get_hint(d, 0), "did you mean MYFUNC?"); + break; + case 4: + ck_assert_str_eq(diag_get_text(d), + "record type REC has no field named FRODO"); + ck_assert_str_eq(diag_get_hint(d, 0), "did you mean FOO?"); + break; + default: + ck_abort_msg("too many diagnostics"); + break; + } +} + +START_TEST(test_typo) +{ + input_from_file(TESTDIR "/parse/typo.vhd"); + + diag_set_consumer(typo_diag_fn); + + tree_t e = parse(); + fail_if(e == NULL); + fail_unless(tree_kind(e) == T_ENTITY); + lib_put(lib_work(), e); + + tree_t a = parse(); + fail_if(a == NULL); + fail_unless(tree_kind(a) == T_ARCH); + + fail_unless(parse() == NULL); + + fail_unless(error_count() == 5); +} +END_TEST + Suite *get_parse_tests(void) { Suite *s = suite_create("parse"); @@ -4944,6 +4997,7 @@ Suite *get_parse_tests(void) tcase_add_test(tc_core, test_visibility5); tcase_add_test(tc_core, test_visibility6); tcase_add_test(tc_core, test_issue479); + tcase_add_test(tc_core, test_typo); suite_add_tcase(s, tc_core); return s; -- 2.39.2