-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Render diagnostics #552
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## feat/diagnostics #552 +/- ##
====================================================
+ Coverage 90.22% 91.09% +0.86%
====================================================
Files 61 61
Lines 6376 6482 +106
====================================================
+ Hits 5753 5905 +152
+ Misses 623 577 -46 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an absolutely beautiful PR, both the error messages and the care that's gone into it. A few thoughts, but I'm happy to leave you to decide what to do about them. (Do think about the long-message after a highlight reaching to nearly the end of the line, though.)
guppylang/diagnostic.py
Outdated
raise InternalGuppyError("Diagnostic: Span label provided without span") | ||
|
||
@property | ||
def rendered_title(self) -> str: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I note that Diagnostic
is basically SubDiagnostic
plus title
/rendered_title
- is it plausible to combine them (make Diagnostic inherit subdiagnostic)?
diagnostic. | ||
""" | ||
values = {f.name: getattr(self, f.name) for f in fields(self)} | ||
return s.format(**values) if s is not None else None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this builds values
(with lots of getattr
) even when it's not needed...(not a big deal for diagnostic code though)
I'm wondering about this Optional[str] -> Optional[str]
though. As a small/first step, you could add a couple of @overload
s (str -> str
and None -> None
) - then I think the assert
in rendered_title
would disappear. (I'm not certain how thoroughly overload
s are actually type-checked though...)
But, I wonder about making render
just str -> str
. Then at the callsites you do return self.long_message and self._render(self.long_message)
. There are not that many callsites, and it gets the right type signatures everywhere without needing overload
or assert
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the overloading idea. Mypy also seems to be happy with it 👍
guppylang/diagnostic.py
Outdated
#: Maximum length of span labels after which we insert a newline | ||
MAX_LABEL_LINE_LEN: Final[int] = 60 | ||
|
||
#: Maximum length of span labels after which we insert a newline |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc/comment here is identical to previous, should it be?
self.buffer += textwrap.wrap( | ||
f"{self.level_str(diag.level)}: {msg}", | ||
self.MAX_MESSAGE_LINE_LEN, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also we omit sub-diagnostics with spans. Is the assumption that there won't be any if the parent doesn't have a span? Ok if you are confident (like, the parent span gets filled in from the child spans), but otherwise you could check / print out all subdiagnostics after the if/else / etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the assumption that there won't be any if the parent doesn't have a span?
I think that's an ok assumption to make for now since basically 99% of diagnostics are going to come with a span. We can come back to that if it turns into an issue later.
I'll add a check though 👍
self.buffer += textwrap.wrap( | ||
f"{diag.rendered_long_message}", self.MAX_MESSAGE_LINE_LEN | ||
) | ||
# Finally, render all sub-diagnostics that have a non-span message |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there something that says sub-diagnostics have either a message OR a span but not both??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc comment of SubDiagnostic.message
says "Message that is printed if no span is provided."
guppylang/diagnostic.py
Outdated
if span.is_multiline: | ||
[first, *middle, last] = span_lines | ||
render_line(first, span.start.line) | ||
# Compute the subspan that only covers the first line and render it's |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super-nit: it's
-> its
render_line(line, span.start.line - prefix_lines + i) | ||
span_lines = all_lines[prefix_lines:] | ||
|
||
if span.is_multiline: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to only include a prefix for multiline spans, or something like that, I dunno, your call, I'm happy either way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, not sure either. This replicates the current behaviour in Guppy, but easy enough to change in the future by updating DiagnosticsRenderer.PREFIX_CONTEXT_LINES
) | ||
render_line(first_highlight) | ||
# Omit everything in the middle | ||
if middle: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if len(middle)>1
, maybe. It'd be silly to omit a single line to display a line ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, but I guess then you'd need to highlight that line as well, hmmm. Ok. Not sure about this, maybe I should just shut up and delete the parent comment ;)
if label: | ||
[label_first, *label_rest] = textwrap.wrap( | ||
label, | ||
self.MAX_LABEL_LINE_LEN, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is the length of the label AFTER the highlight bar. Irrespective of how much space there is left on the line after said highlight bar. How about instead you ask textwap to layout highlight_char * len(last_span) + " " + label
with initial indent last_span.start.column
and subsequent indents, hmmm, you might want to go backwards ?
^^^^ there is |
some problem here |
that won't fit on the|
rest of the line
(|
to indicate line end boundary, not printed)
Think if your test_long_label
has very little space after the ==
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the current alignment looks nicer, but I agree that the spacing might become problematic.
In a previous version I implemented a layout algorithm that abbreviates the start/end of the line and/or the middle of the span with ...
ro ensure that the ^^^
doesn't go too far to the right and everything fits into a specified width
|
42 | ...else foo(looo...ng_id).more_stuff(...
| ^^^^^^^^^^^^ Span label with
| line break
I think that is the best solution but would have made this PR 150-200 lines longer 😅
I created #556 as a followup
render_line(last_highlight) | ||
|
||
@staticmethod | ||
def level_str(level: DiagnosticLevel) -> str: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't DiagnosticLevel.to_string
/ format / something ?
Closes #537