Commit d1b5f5cc by David Malcolm Committed by David Malcolm

New fix-it printer

The existing fix-it printer can lead to difficult-to-read output
when fix-it hints are near each other.  For example, in a recent
patch to add fix-it hints to the C++ frontend's -Wold-style-cast,
e.g. for:

  foo *f = (foo *)ptr->field;
                       ^~~~~

the fix-it hints:
 replace the open paren with "const_cast<"
 replace the close paren with "> ("
 insert ")" after the "ptr->field"

would be printed in this odd-looking way:

  foo *f = (foo *)ptr->field;
                       ^~~~~
           -
           const_cast<
                 -
                 > (        )

class rich_location consolidates adjacent fix-it hints, which helps
somewhat, but the underlying problem is that the existing printer
simply walks through the list of hints printing them, starting newlines
as necessary.

This patch reimplements fix-it printing by introducing a planning
stage: a new class line_corrections "plans" how to print the
fix-it hints affecting a line, generating a vec of "correction"
instances.  Hints that are sufficiently close to each other are
consolidated at this stage.

This leads to the much more reasonable output for the above case:

  foo *f = (foo *)ptr->field;
                       ^~~~~
           -----------------
           const_cast<foo *> (ptr->field);

where the 3 hints are consolidated into one "correction" at printing.

gcc/ChangeLog:
	* diagnostic-show-locus.c (struct column_range): New struct.
	(get_affected_columns): New function.
	(get_printed_columns): New function.
	(struct correction): New struct.
	(correction::ensure_capacity): New function.
	(correction::ensure_terminated): New function.
	(struct line_corrections): New struct.
	(line_corrections::~line_corrections): New dtor.
	(line_corrections::add_hint): New function.
	(layout::print_trailing_fixits): Reimplement in terms of the new
	classes.
	(selftest::test_overlapped_fixit_printing): New function.
	(selftest::diagnostic_show_locus_c_tests): Call it.

From-SVN: r247548
parent 5bb64c41
2017-05-03 David Malcolm <dmalcolm@redhat.com>
* diagnostic-show-locus.c (struct column_range): New struct.
(get_affected_columns): New function.
(get_printed_columns): New function.
(struct correction): New struct.
(correction::ensure_capacity): New function.
(correction::ensure_terminated): New function.
(struct line_corrections): New struct.
(line_corrections::~line_corrections): New dtor.
(line_corrections::add_hint): New function.
(layout::print_trailing_fixits): Reimplement in terms of the new
classes.
(selftest::test_overlapped_fixit_printing): New function.
(selftest::diagnostic_show_locus_c_tests): Call it.
2017-05-03 Nathan Sidwell <nathan@acm.org> 2017-05-03 Nathan Sidwell <nathan@acm.org>
Canonicalize canonical type hashing Canonicalize canonical type hashing
......
...@@ -1242,6 +1242,295 @@ layout::annotation_line_showed_range_p (int line, int start_column, ...@@ -1242,6 +1242,295 @@ layout::annotation_line_showed_range_p (int line, int start_column,
return false; return false;
} }
/* Classes for printing trailing fix-it hints i.e. those that
don't add new lines.
For insertion, these can look like:
new_text
For replacement, these can look like:
------------- : underline showing affected range
new_text
For deletion, these can look like:
------------- : underline showing affected range
This can become confusing if they overlap, and so we need
to do some preprocessing to decide what to print.
We use the list of fixit_hint instances affecting the line
to build a list of "correction" instances, and print the
latter.
For example, consider a set of fix-its for converting
a C-style cast to a C++ const_cast.
Given:
..000000000111111111122222222223333333333.
..123456789012345678901234567890123456789.
foo *f = (foo *)ptr->field;
^~~~~
and the fix-it hints:
- replace col 10 (the open paren) with "const_cast<"
- replace col 16 (the close paren) with "> ("
- insert ")" before col 27
then we would get odd-looking output:
foo *f = (foo *)ptr->field;
^~~~~
-
const_cast<
-
> ( )
It would be better to detect when fixit hints are going to
overlap (those that require new lines), and to consolidate
the printing of such fixits, giving something like:
foo *f = (foo *)ptr->field;
^~~~~
-----------------
const_cast<foo *> (ptr->field)
This works by detecting when the printing would overlap, and
effectively injecting no-op replace hints into the gaps between
such fix-its, so that the printing joins up.
In the above example, the overlap of:
- replace col 10 (the open paren) with "const_cast<"
and:
- replace col 16 (the close paren) with "> ("
is fixed by injecting a no-op:
- replace cols 11-15 with themselves ("foo *")
and consolidating these, making:
- replace cols 10-16 with "const_cast<" + "foo *" + "> ("
i.e.:
- replace cols 10-16 with "const_cast<foo *> ("
This overlaps with the final fix-it hint:
- insert ")" before col 27
and so we repeat the consolidation process, by injecting
a no-op:
- replace cols 17-26 with themselves ("ptr->field")
giving:
- replace cols 10-26 with "const_cast<foo *> (" + "ptr->field" + ")"
i.e.:
- replace cols 10-26 with "const_cast<foo *> (ptr->field)"
and is thus printed as desired. */
/* A range of columns within a line. */
struct column_range
{
column_range (int start_, int finish_) : start (start_), finish (finish_) {}
bool operator== (const column_range &other) const
{
return start == other.start && finish == other.finish;
}
int start;
int finish;
};
/* Get the range of columns that HINT would affect. */
static column_range
get_affected_columns (const fixit_hint *hint)
{
int start_column = LOCATION_COLUMN (hint->get_start_loc ());
int finish_column = LOCATION_COLUMN (hint->get_next_loc ()) - 1;
return column_range (start_column, finish_column);
}
/* Get the range of columns that would be printed for HINT. */
static column_range
get_printed_columns (const fixit_hint *hint)
{
int start_column = LOCATION_COLUMN (hint->get_start_loc ());
int final_hint_column = start_column + hint->get_length () - 1;
if (hint->insertion_p ())
{
return column_range (start_column, final_hint_column);
}
else
{
int finish_column = LOCATION_COLUMN (hint->get_next_loc ()) - 1;
return column_range (start_column,
MAX (finish_column, final_hint_column));
}
}
/* A correction on a particular line.
This describes a plan for how to print one or more fixit_hint
instances that affected the line, potentially consolidating hints
into corrections to make the result easier for the user to read. */
struct correction
{
correction (column_range affected_columns,
column_range printed_columns,
const char *new_text, size_t new_text_len)
: m_affected_columns (affected_columns),
m_printed_columns (printed_columns),
m_text (xstrdup (new_text)),
m_len (new_text_len),
m_alloc_sz (new_text_len + 1)
{
}
~correction () { free (m_text); }
bool insertion_p () const
{
return m_affected_columns.start == m_affected_columns.finish + 1;
}
void ensure_capacity (size_t len);
void ensure_terminated ();
/* If insert, then start: the column before which the text
is to be inserted, and finish is offset by the length of
the replacement.
If replace, then the range of columns affected. */
column_range m_affected_columns;
/* If insert, then start: the column before which the text
is to be inserted, and finish is offset by the length of
the replacement.
If replace, then the range of columns affected. */
column_range m_printed_columns;
/* The text to be inserted/used as replacement. */
char *m_text;
size_t m_len;
size_t m_alloc_sz;
};
/* Ensure that m_text can hold a string of length LEN
(plus 1 for 0-termination). */
void
correction::ensure_capacity (size_t len)
{
/* Allow 1 extra byte for 0-termination. */
if (m_alloc_sz < (len + 1))
{
size_t new_alloc_sz = (len + 1) * 2;
m_text = (char *)xrealloc (m_text, new_alloc_sz);
m_alloc_sz = new_alloc_sz;
}
}
/* Ensure that m_text is 0-terminated. */
void
correction::ensure_terminated ()
{
/* 0-terminate the buffer. */
gcc_assert (m_len < m_alloc_sz);
m_text[m_len] = '\0';
}
/* A list of corrections affecting a particular line.
This is used by layout::print_trailing_fixits for planning
how to print the fix-it hints affecting the line. */
struct line_corrections
{
line_corrections (const char *filename, int row)
: m_filename (filename), m_row (row)
{}
~line_corrections ();
void add_hint (const fixit_hint *hint);
const char *m_filename;
int m_row;
auto_vec <correction *> m_corrections;
};
/* struct line_corrections. */
line_corrections::~line_corrections ()
{
unsigned i;
correction *c;
FOR_EACH_VEC_ELT (m_corrections, i, c)
delete c;
}
/* Add HINT to the corrections for this line.
Attempt to consolidate nearby hints so that they will not
overlap with printed. */
void
line_corrections::add_hint (const fixit_hint *hint)
{
column_range affected_columns = get_affected_columns (hint);
column_range printed_columns = get_printed_columns (hint);
/* Potentially consolidate. */
if (!m_corrections.is_empty ())
{
correction *last_correction
= m_corrections[m_corrections.length () - 1];
if (printed_columns.start <= last_correction->m_printed_columns.finish)
{
/* We have two hints for which the printed forms of the hints
would touch or overlap, so we need to consolidate them to avoid
confusing the user.
Attempt to inject a "replace" correction from immediately
after the end of the last hint to immediately before the start
of the next hint. */
column_range between (last_correction->m_affected_columns.finish + 1,
printed_columns.start - 1);
/* Try to read the source. */
int line_width;
const char *line = location_get_source_line (m_filename, m_row,
&line_width);
if (line && between.finish < line_width)
{
/* Consolidate into the last correction:
add a no-op "replace" of the "between" text, and
add the text from the new hint. */
size_t old_len = last_correction->m_len;
size_t between_len = between.finish + 1 - between.start;
size_t new_len = old_len + between_len + hint->get_length ();
last_correction->ensure_capacity (new_len);
memcpy (last_correction->m_text + old_len,
line + between.start - 1,
between.finish + 1 - between.start);
memcpy (last_correction->m_text + old_len + between_len,
hint->get_string (), hint->get_length ());
last_correction->m_len = new_len;
last_correction->ensure_terminated ();
last_correction->m_affected_columns.finish
= affected_columns.finish;
last_correction->m_printed_columns.finish
+= between_len + hint->get_length ();
return;
}
}
}
/* If no consolidation happened, add a new correction instance. */
m_corrections.safe_push (new correction (affected_columns,
printed_columns,
hint->get_string (),
hint->get_length ()));
}
/* If there are any fixit hints on source line ROW, print them. /* If there are any fixit hints on source line ROW, print them.
They are printed in order, attempting to combine them onto lines, but They are printed in order, attempting to combine them onto lines, but
starting new lines if necessary. starting new lines if necessary.
...@@ -1251,40 +1540,50 @@ layout::annotation_line_showed_range_p (int line, int start_column, ...@@ -1251,40 +1540,50 @@ layout::annotation_line_showed_range_p (int line, int start_column,
void void
layout::print_trailing_fixits (int row) layout::print_trailing_fixits (int row)
{ {
int column = 0; /* Build a list of correction instances for the line,
potentially consolidating hints (for the sake of readability). */
line_corrections corrections (m_exploc.file, row);
for (unsigned int i = 0; i < m_fixit_hints.length (); i++) for (unsigned int i = 0; i < m_fixit_hints.length (); i++)
{ {
const fixit_hint *hint = m_fixit_hints[i]; const fixit_hint *hint = m_fixit_hints[i];
/* Newline fixits are handled by layout::print_leading_fixits. */
if (hint->ends_with_newline_p ()) if (hint->ends_with_newline_p ())
continue; continue;
if (hint->affects_line_p (m_exploc.file, row)) if (hint->affects_line_p (m_exploc.file, row))
corrections.add_hint (hint);
}
/* Now print the corrections. */
unsigned i;
correction *c;
int column = 0;
FOR_EACH_VEC_ELT (corrections.m_corrections, i, c)
{ {
/* For now we assume each fixit hint can only touch one line. */ /* For now we assume each fixit hint can only touch one line. */
if (hint->insertion_p ()) if (c->insertion_p ())
{ {
/* This assumes the insertion just affects one line. */ /* This assumes the insertion just affects one line. */
int start_column = LOCATION_COLUMN (hint->get_start_loc ()); int start_column = c->m_printed_columns.start;
move_to_column (&column, start_column); move_to_column (&column, start_column);
m_colorizer.set_fixit_insert (); m_colorizer.set_fixit_insert ();
pp_string (m_pp, hint->get_string ()); pp_string (m_pp, c->m_text);
m_colorizer.set_normal_text (); m_colorizer.set_normal_text ();
column += hint->get_length (); column += c->m_len;
} }
else else
{ {
int line = LOCATION_LINE (hint->get_start_loc ());
int start_column = LOCATION_COLUMN (hint->get_start_loc ());
int finish_column = LOCATION_COLUMN (hint->get_next_loc ()) - 1;
/* If the range of the replacement wasn't printed in the /* If the range of the replacement wasn't printed in the
annotation line, then print an extra underline to annotation line, then print an extra underline to
indicate exactly what is being replaced. indicate exactly what is being replaced.
Always show it for removals. */ Always show it for removals. */
if (!annotation_line_showed_range_p (line, start_column, int start_column = c->m_affected_columns.start;
int finish_column = c->m_affected_columns.finish;
if (!annotation_line_showed_range_p (row, start_column,
finish_column) finish_column)
|| hint->get_length () == 0) || c->m_len == 0)
{ {
move_to_column (&column, start_column); move_to_column (&column, start_column);
m_colorizer.set_fixit_delete (); m_colorizer.set_fixit_delete ();
...@@ -1295,14 +1594,13 @@ layout::print_trailing_fixits (int row) ...@@ -1295,14 +1594,13 @@ layout::print_trailing_fixits (int row)
/* Print the replacement text. REPLACE also covers /* Print the replacement text. REPLACE also covers
removals, so only do this extra work (potentially starting removals, so only do this extra work (potentially starting
a new line) if we have actual replacement text. */ a new line) if we have actual replacement text. */
if (hint->get_length () > 0) if (c->m_len > 0)
{ {
move_to_column (&column, start_column); move_to_column (&column, start_column);
m_colorizer.set_fixit_insert (); m_colorizer.set_fixit_insert ();
pp_string (m_pp, hint->get_string ()); pp_string (m_pp, c->m_text);
m_colorizer.set_normal_text (); m_colorizer.set_normal_text ();
column += hint->get_length (); column += c->m_len;
}
} }
} }
} }
...@@ -2153,6 +2451,203 @@ test_fixit_consolidation (const line_table_case &case_) ...@@ -2153,6 +2451,203 @@ test_fixit_consolidation (const line_table_case &case_)
} }
} }
/* Verify that the line_corrections machinery correctly prints
overlapping fixit-hints. */
static void
test_overlapped_fixit_printing (const line_table_case &case_)
{
/* Create a tempfile and write some text to it.
...000000000111111111122222222223333333333.
...123456789012345678901234567890123456789. */
const char *content
= (" foo *f = (foo *)ptr->field;\n");
temp_source_file tmp (SELFTEST_LOCATION, ".C", content);
line_table_test ltt (case_);
const line_map_ordinary *ord_map
= linemap_check_ordinary (linemap_add (line_table, LC_ENTER, false,
tmp.get_filename (), 0));
linemap_line_start (line_table, 1, 100);
const location_t final_line_end
= linemap_position_for_line_and_column (line_table, ord_map, 6, 36);
/* Don't attempt to run the tests if column data might be unavailable. */
if (final_line_end > LINE_MAP_MAX_LOCATION_WITH_COLS)
return;
/* A test for converting a C-style cast to a C++-style cast. */
const location_t open_paren
= linemap_position_for_line_and_column (line_table, ord_map, 1, 12);
const location_t close_paren
= linemap_position_for_line_and_column (line_table, ord_map, 1, 18);
const location_t expr_start
= linemap_position_for_line_and_column (line_table, ord_map, 1, 19);
const location_t expr_finish
= linemap_position_for_line_and_column (line_table, ord_map, 1, 28);
const location_t expr = make_location (expr_start, expr_start, expr_finish);
/* Various examples of fix-it hints that aren't themselves consolidated,
but for which the *printing* may need consolidation. */
/* Example where 3 fix-it hints are printed as one. */
{
test_diagnostic_context dc;
rich_location richloc (line_table, expr);
richloc.add_fixit_replace (open_paren, "const_cast<");
richloc.add_fixit_replace (close_paren, "> (");
richloc.add_fixit_insert_after (")");
diagnostic_show_locus (&dc, &richloc, DK_ERROR);
ASSERT_STREQ ("\n"
" foo *f = (foo *)ptr->field;\n"
" ^~~~~~~~~~\n"
" -----------------\n"
" const_cast<foo *> (ptr->field)\n",
pp_formatted_text (dc.printer));
/* Unit-test the line_corrections machinery. */
ASSERT_EQ (3, richloc.get_num_fixit_hints ());
const fixit_hint *hint_0 = richloc.get_fixit_hint (0);
ASSERT_EQ (column_range (12, 12), get_affected_columns (hint_0));
ASSERT_EQ (column_range (12, 22), get_printed_columns (hint_0));
const fixit_hint *hint_1 = richloc.get_fixit_hint (1);
ASSERT_EQ (column_range (18, 18), get_affected_columns (hint_1));
ASSERT_EQ (column_range (18, 20), get_printed_columns (hint_1));
const fixit_hint *hint_2 = richloc.get_fixit_hint (2);
ASSERT_EQ (column_range (29, 28), get_affected_columns (hint_2));
ASSERT_EQ (column_range (29, 29), get_printed_columns (hint_2));
/* Add each hint in turn to a line_corrections instance,
and verify that they are consolidated into one correction instance
as expected. */
line_corrections lc (tmp.get_filename (), 1);
/* The first replace hint by itself. */
lc.add_hint (hint_0);
ASSERT_EQ (1, lc.m_corrections.length ());
ASSERT_EQ (column_range (12, 12), lc.m_corrections[0]->m_affected_columns);
ASSERT_EQ (column_range (12, 22), lc.m_corrections[0]->m_printed_columns);
ASSERT_STREQ ("const_cast<", lc.m_corrections[0]->m_text);
/* After the second replacement hint, they are printed together
as a replacement (along with the text between them). */
lc.add_hint (hint_1);
ASSERT_EQ (1, lc.m_corrections.length ());
ASSERT_STREQ ("const_cast<foo *> (", lc.m_corrections[0]->m_text);
ASSERT_EQ (column_range (12, 18), lc.m_corrections[0]->m_affected_columns);
ASSERT_EQ (column_range (12, 30), lc.m_corrections[0]->m_printed_columns);
/* After the final insertion hint, they are all printed together
as a replacement (along with the text between them). */
lc.add_hint (hint_2);
ASSERT_STREQ ("const_cast<foo *> (ptr->field)",
lc.m_corrections[0]->m_text);
ASSERT_EQ (1, lc.m_corrections.length ());
ASSERT_EQ (column_range (12, 28), lc.m_corrections[0]->m_affected_columns);
ASSERT_EQ (column_range (12, 41), lc.m_corrections[0]->m_printed_columns);
}
/* Example where two are consolidated during printing. */
{
test_diagnostic_context dc;
rich_location richloc (line_table, expr);
richloc.add_fixit_replace (open_paren, "CAST (");
richloc.add_fixit_replace (close_paren, ") (");
richloc.add_fixit_insert_after (")");
diagnostic_show_locus (&dc, &richloc, DK_ERROR);
ASSERT_STREQ ("\n"
" foo *f = (foo *)ptr->field;\n"
" ^~~~~~~~~~\n"
" -\n"
" CAST (-\n"
" ) ( )\n",
pp_formatted_text (dc.printer));
}
/* Example where none are consolidated during printing. */
{
test_diagnostic_context dc;
rich_location richloc (line_table, expr);
richloc.add_fixit_replace (open_paren, "CST (");
richloc.add_fixit_replace (close_paren, ") (");
richloc.add_fixit_insert_after (")");
diagnostic_show_locus (&dc, &richloc, DK_ERROR);
ASSERT_STREQ ("\n"
" foo *f = (foo *)ptr->field;\n"
" ^~~~~~~~~~\n"
" -\n"
" CST ( -\n"
" ) ( )\n",
pp_formatted_text (dc.printer));
}
/* Example of deletion fix-it hints. */
{
test_diagnostic_context dc;
rich_location richloc (line_table, expr);
richloc.add_fixit_insert_before (open_paren, "(bar *)");
source_range victim = {open_paren, close_paren};
richloc.add_fixit_remove (victim);
/* This case is actually handled by fixit-consolidation,
rather than by line_corrections. */
ASSERT_EQ (1, richloc.get_num_fixit_hints ());
diagnostic_show_locus (&dc, &richloc, DK_ERROR);
ASSERT_STREQ ("\n"
" foo *f = (foo *)ptr->field;\n"
" ^~~~~~~~~~\n"
" -------\n"
" (bar *)\n",
pp_formatted_text (dc.printer));
}
/* Example of deletion fix-it hints that would overlap. */
{
test_diagnostic_context dc;
rich_location richloc (line_table, expr);
richloc.add_fixit_insert_before (open_paren, "(longer *)");
source_range victim = {expr_start, expr_finish};
richloc.add_fixit_remove (victim);
/* These fixits are not consolidated. */
ASSERT_EQ (2, richloc.get_num_fixit_hints ());
/* But the corrections are. */
diagnostic_show_locus (&dc, &richloc, DK_ERROR);
ASSERT_STREQ ("\n"
" foo *f = (foo *)ptr->field;\n"
" ^~~~~~~~~~\n"
" -----------------\n"
" (longer *)(foo *)\n",
pp_formatted_text (dc.printer));
}
/* Example of insertion fix-it hints that would overlap. */
{
test_diagnostic_context dc;
rich_location richloc (line_table, expr);
richloc.add_fixit_insert_before (open_paren, "LONGER THAN THE CAST");
richloc.add_fixit_insert_after (close_paren, "TEST");
/* The first insertion is long enough that if printed naively,
it would overlap with the second.
Verify that they are printed as a single replacement. */
diagnostic_show_locus (&dc, &richloc, DK_ERROR);
ASSERT_STREQ ("\n"
" foo *f = (foo *)ptr->field;\n"
" ^~~~~~~~~~\n"
" -------\n"
" LONGER THAN THE CAST(foo *)TEST\n",
pp_formatted_text (dc.printer));
}
}
/* Insertion fix-it hint: adding a "break;" on a line by itself. */ /* Insertion fix-it hint: adding a "break;" on a line by itself. */
static void static void
...@@ -2314,6 +2809,7 @@ diagnostic_show_locus_c_tests () ...@@ -2314,6 +2809,7 @@ diagnostic_show_locus_c_tests ()
for_each_line_table_case (test_diagnostic_show_locus_one_liner); for_each_line_table_case (test_diagnostic_show_locus_one_liner);
for_each_line_table_case (test_diagnostic_show_locus_fixit_lines); for_each_line_table_case (test_diagnostic_show_locus_fixit_lines);
for_each_line_table_case (test_fixit_consolidation); for_each_line_table_case (test_fixit_consolidation);
for_each_line_table_case (test_overlapped_fixit_printing);
for_each_line_table_case (test_fixit_insert_containing_newline); for_each_line_table_case (test_fixit_insert_containing_newline);
for_each_line_table_case (test_fixit_insert_containing_newline_2); for_each_line_table_case (test_fixit_insert_containing_newline_2);
for_each_line_table_case (test_fixit_replace_containing_newline); for_each_line_table_case (test_fixit_replace_containing_newline);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment