-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Move full diagnostic rendering to ruff_db
#19415
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
Conversation
|
|
Thank you for looking into this. I'm not sure I understand what the diff shown for Diagnostics with FixAvailability::Never or Sometimes has to do with the fix availability. I think it would be more helpful to list the separate differences:
|
We can't change the range or that would be breaking where suppression comments are allowed. We also can't remove the range or the diagnostic can no longer be suppressed. What I don't understand in your example. Is the rendered code frame just empty? Does it collapse lines or does it render the full file? |
Okay, I think I understand now what's happening. The issue is that ruff uses empty ranges for file level diagnostics. I think the ideal output would be to only render the file name if both the range and message are empty. We could decide to make
@BurntSushi might be able to help but you might have to bite the bullet and try to find the source of the empty line yourself. It's also worth double checking if the new renderer handles these tricky cases
|
Oh sorry, I was using that as a proxy for having a
Oops, this was really poorly phrased, but I think you still figured out what I meant in your last comment. Yes, we use ruff/crates/ruff_linter/src/message/text.rs Lines 119 to 121 in 6403eba
I tried making those
I totally agree. I got close to that, modulo the blank line I need to track down in - -:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
- -:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
+ [RUF901]: [*] Hey this is a stable test rule with a safe fix.
+
+ [RUF902]: Hey this is a stable test rule with an unsafe fix.
+I think there was a filename, but maybe not. I'd need to figure that out too. I don't think we're even passing anything on stdin in these tests, so the three-line code frame isn't even empty lines in the file, it's entirely synthetic. Thanks for the tricky cases. They look similar to some of Andrew's other code I've run across in |
.../rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B911_B911.py.snap
Outdated
Show resolved
Hide resolved
|
I looked through my commit history here, but couldn't find anything directly related to file-level diagnostics. There were some changes related to tweaking ranges and even one phantom line terminator (although this came from our code and not |
Summary -- This PR tweaks Ruff's internal usage of the new diagnostic model to more closely match the intended use, as I understand it. Specifically, it moves the fix/help suggestion from the primary annotation's message to a subdiagnostic. In turn, it adds the secondary/noqa code as the new primary annotation message. As shown in the new `ruff_db` tests, this more closely mirrors Ruff's current diagnostic output. I also added `Severity::Help` to render the fix suggestion with a `help:` prefix instead of `info:`. These changes don't have any external impact now but should help a bit with #19415. Test Plan -- New full output format tests in `ruff_db` Rendered Diagnostics -- Full diagnostic output from `annotate-snippets` in this PR: ``` error[unused-import]: `os` imported but unused --> fib.py:1:8 | 1 | import os | ^^ | help: Remove unused import: `os` ``` Current Ruff output for the same code: ``` fib.py:1:8: F401 [*] `os` imported but unused | 1 | import os | ^^ F401 | = help: Remove unused import: `os` ``` Proposed final output after #19415: ``` F401 [*] `os` imported but unused --> fib.py:1:8 | 1 | import os | ^^ | help: Remove unused import: `os` ``` These are slightly updated from #19464 (comment) below to remove the extra noqa codes in the primary annotation messages for the first and third cases.
4fb83b2 to
f26af05
Compare
|
I think I'm getting pretty close to having the output we want, albeit with a potentially hacky implementation. For this input: import math
x: str = 1
match 42:
case _: ...and this shell command: git diff <(ruff check --no-cache tr{i,y}.py --target-version py39) \
<(myruff check --no-cache tr{i,y}.py --target-version py39)I'm currently getting this diff: -tri.py:1:1: E902 No such file or directory (os error 2)
-try.py:1:8: F401 [*] `math` imported but unused
+E902 No such file or directory (os error 2)
+
+F401 [*] `math` imported but unused
+ --> try.py:1:8
|
1 | import math
- | ^^^^ F401
+ | ^^^^
2 |
3 | x: str = 1
|
- = help: Remove unused import: `math`
+help: Remove unused import: `math`
-try.py:5:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+ --> try.py:5:1
|
3 | x: str = 1
4 |This all looks good to me except the missing filename for the full-file diagnostic E902. I also tried a bit to get rid of the extra newline without much luck. I think the last two commits Andrew linked are in exactly the right place, but reverting to the version in 602a27c removes all the lines between diagnostics. I think I've convinced myself that it's actually nice to have the "extra" line for E902 since that's consistent with other diagnostics, but maybe I'm just giving up too easily. Another issue with the check I added to remove the empty annotations is that some ruff/crates/ty_ide/src/goto_declaration.rs Lines 289 to 294 in dc10ab8
when I think it might want to point to the beginning of the file, with a real annotation. I002 is another example of a diagnostic that might actually want a default range and changed in my recent commits: Lines 5 to 8 in dc10ab8
I'll keep looking at these remaining issues tomorrow, but at least I made some progress on the brackets and blank lines (maybe) today. |
I think that actually looks fine. Have you tried how Regarding the full range diagnostics. This is obviously something I'd like @BurntSushi's input on before changing but I wonder if the problem here really is that the primary annotation range does:
Ideally, the diagnostic model would allow a rule to set a span for 1 but without enabling 2. The first idea that comes to mind is to add a span to An alternative is to mark a primary annotation to say, nope, only render the file name but no message or annotation. This does seem a bit weird given that that's exactly what an annotation is for but again, something we could enforce with an assertion somewhere. I think the biggest challenge here might is probably that annotation snippet doesn't allow rendering the file name without a "snippet". We could probably work around this by simply copy pasting the file name rendering. What's unclear to me from a UX perspective if the following is confusing: |
What do you mean without an annotation? I think in a literal, but maybe unhelpful, sense all Ruff diagnostics have annotations. Here's the full output of the ruff command above with a couple of extra non-existent files, if that helps:
I think that's a very good summary of the problem. And thanks for the other ideas, those seem helpful to explore.
Yeah, I do like Ruff's current rendering here, basically collapsing back to concise rendering for E902: But that's also consistent with its full output. I'm not sure if mixing the two is confusing too: Ah, it looks like rustc just embeds the filename in the error message when there's no annotation: But then we'd have to modify the message on |
|
I think adding a knob on to I do agree that this makes the name "annotation" somewhat of a misnomer in certain cases. Although, you could recast as, "it's still an annotation, but it applies to contents that are too large to usefully display in an error message." |
867cff1 to
b2842bc
Compare
|
We've got filenames! -tri.py:1:1: E902 No such file or directory (os error 2)
-try.py:1:8: F401 [*] `math` imported but unused
+E902 No such file or directory (os error 2)
+--> tri.py:1:1
+
+F401 [*] `math` imported but unused
+ --> try.py:1:8
|
1 | import math
- | ^^^^ F401
+ | ^^^^
2 |
3 | x: str = 1
|
- = help: Remove unused import: `math`
+help: Remove unused import: `math`
-try.py:5:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
+ --> try.py:5:1
|
3 | x: str = 1
4 |Plumbing this setting through the I think the diagnostic output is where I want it now. I'll look over the implementation again and hopefully open it for review later today! |
|
I think the implementation looks okay. It's mostly just passing flags (that might need better names!) into annotate-snippets and some touchy formatting changes based on them. I also looked closely at the tricky cases Micha mentioned above. The cut indicator ellipsis was easy thanks to #19420, I just had to copy that over to the new file. That also reverted the remaining snapshot changes in ty crates. For the I'm a bit stuck on where to inject the All of the code changes except the line terminator fix in the last commit are in this range. I'll update the summary before marking this ready for review. |
ruff_dbruff_db
|
Would it make sense to split this pr into 1) extending the rendering and b) migrating ruff? Or can you organize the commits in a way that makes reviewing easier |
01af353 to
3e1ad1b
Compare
MichaReiser
left a comment
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 didn't review all snapshot changes but this looks great. Thank you for patiently rebasing this PR and splitting out your fixes into smaller PRs!
| } | ||
|
|
||
| if self.flags.intersects(EmitterFlags::SHOW_FIX_DIFF) { | ||
| if self.show_fix_diff { |
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 you haven't done so already. Can you double check if the diff rendering still looks good?
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 the diff rendering that only works in the snapshots (#7352). I didn't notice any changes in those sections of the snapshots, as expected, but is there something else you want me to check here?
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, I thought it's the CLI's --diff mode. Also, is there any existing code that we can delete, now that we moved the diagnostic rendering (e.g. MessageCodeFrame)?
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.
No the --diff flag is separate and looks good:
> ruff check try.py --diff
--- try.py
+++ try.py
@@ -1 +0,0 @@
-import math
Would fix 1 error.
> myruff check try.py --diff
--- try.py
+++ try.py
@@ -1 +0,0 @@
-import math
Would fix 1 error.We can't quite delete the TextEmitter and the MessageCodeFrame rendering because it's still used by the grouped format. Once we move that over, we can delete a big chunk of code.
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.
It seems that GroupEmitter::with_show_source is only called in tests. So we could just delete it? But I also don't mind if you do this in a separate PR
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 you're right, I forgot about that one. I'll check again for unused code and follow up!
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.
Sounds good. I'm fine merging this PR and follow up in a separate PR.
|
This should be ready for review. I left the commits towards the end named as if they resolve the carriage return, BOM, and EOF issues, but after #19806 they just accept the snapshots from the actual code changes in the other PR. I also wrote a much shorter AWK version of Claude's script for checking the offsets in the diff. It has three false positives, but I verified that they are just due to git displaying a larger diff in those areas, not real differences in Script and output
#!/usr/bin/awk -f
{
# Old line like:
# - -:1:8: F401 [*] `os` imported but unused
if (match($0, /-.*:([0-9]+):([0-9]+):/, m)) {
row = m[1]
col = m[2]
line = $0
}
# New line like:
# + F401 [*] `os` imported but unused
else if (match($0, /+.*-->.*:([0-9]+):([0-9]+)/, m)) {
new_row = m[1]
new_col = m[2]
if (new_row != row || new_col != col) {
print "MISMATCH:"
printf "\tOld line: %s\n", line
printf "\tNew line: %s\n", $0
}
}
}These are false positives due to how git reports the diff for these files. $ git diff main | ./check_offsets.awk
MISMATCH:
Old line: -E501_1.py:5:89: E501 Line too long (150 > 88)
New line: + --> E501_1.py:4:89
MISMATCH:
Old line: -E501_1.py:6:89: E501 Line too long (149 > 88)
New line: + --> E501_1.py:5:89
MISMATCH:
Old line: -all.py:1:5: D103 Missing docstring in public function
New line: +--> all.py:1:1For example: -all.py:1:1: D100 Missing docstring in public module
-all.py:1:5: D103 Missing docstring in public function
+D100 Missing docstring in public module
+--> all.py:1:1
+
+D103 Missing docstring in public function
+ --> all.py:1:5The E501 case is similar. Basically all of the E501 diagnostics for the file are jumbled together in the diff because the long-line truncation changes too, but all of the individual diagnostics are still there and reported with the correct line and column numbers. |
BurntSushi
left a comment
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.
LGTM!
## Summary This is a follow-up to #19415 (comment) to remove some unused code. As Micha noticed, `GroupedEmitter::with_show_source` was only used in local unit tests[^1] and was safe to remove. This allowed deleting `MessageCodeFrame` and a lot more helper code previously shared with the `full` output format. I also moved some other code from `text.rs` and `message/mod.rs` into `grouped.rs` that is now only used for the `grouped` format. With a little refactoring of the `concise` rendering logic in `ruff_db`, we could probably remove `RuleCodeAndBody` too. The only difference I see from the `concise` output is whether we print the filename next to the row and column or not: ```shell > ruff check --output-format concise try.py:1:8: F401 [*] `math` imported but unused > ruff check --output-format grouped try.py: 1:8 F401 [*] `math` imported but unused ``` But I didn't try to do that here. ## Test Plan Existing tests, with the source code no longer displayed. I also deleted one test, as it was now a duplicate of the `default` test. [^1]: "Local unit tests" as opposed to all of our linter snapshot tests, as is the case for `TextEmitter::with_show_fix_diff`. We also want to expose that to users eventually (#7352), which I don't believe is the case for the `grouped` format.
Summary
This PR switches the
fulloutput format in Ruff over to use the rendering codein
ruff_db. As proposed in the design doc, this involves a lot ofchanges to the snapshot output.
I also had to comment out this assertion with a TODO to replace it after #19688 because many of Ruff's "file-level" annotations aren't actually file-level. They just happen to occur at the start of the file, especially in tests with very short snippets.
ruff/crates/ruff_annotate_snippets/src/renderer/display_list.rs
Lines 1204 to 1208 in 529d81d
I broke up the snapshot commits at the end into several blocks, but I don't think it's enough to help with review. The first few (notebooks, syntax errors, and test rules) are small enough to look at, but I couldn't really think of other categories beyond that. I'm happy to break those up or pick out specific examples beyond what I have below, if that would help.
The minimal code changes are in this range, with the snapshot commits following. Moving the
FullRendererand updating theEmitterFlagsaren't strictly necessary either. I even dropped the renderer commit this morning but figured it made sense to keep it since we have thefullmodule for tests. I don't feel strongly either way.Test Plan
I did actually click through all 1700 snapshots individually instead of
accepting them all at once, although I moved through them quickly. There are a
few main categories:
Lint diagnostics
Syntax errors
These are much like the above.
One thing I noticed while reviewing some of these, but I don't think is strictly syntax-error-related, is that some of the new diagnostics have a little less context after the error. I don't think this is a problem, but it's one small discrepancy I hadn't noticed before. Here's a minor example:
And one of the biggest examples:
Similarly, a few of the lint diagnostics showed that the cut indicator calculation for overly long lines is also slightly different, but I think that's okay too.
Full-file diagnostics
As noted above, these will be much more rare after #19688 too. This case isn't a true full-file diagnostic and will render a snippet in the future, but you can see that we're now rendering the help message that would have been discarded before. In contrast, this is a true full-file diagnostic and should still look like this after #19688:
Jupyter notebooks
There's nothing particularly different about these, just showing off the cell index again.