Skip to content

Conversation

@ntBre
Copy link
Contributor

@ntBre ntBre commented Jul 11, 2025

Summary

Another output format like #19133. This is the reviewdog output format, which is somewhat similar to regular JSON. Like #19270, in the first commit I converted from using json! to Serialize structs, then in the second commit I moved the module to ruff_db.

The reviewdog schema seems a bit more flexible than our JSON schema, so I'm not sure if we need any preview checks here. I'll flag the places I wasn't sure about as review comments.

Test Plan

New tests in rdjson.rs, ported from the old rjdson.rs module, as well as the new CLI output tests.

@ntBre ntBre added internal An internal refactor or improvement diagnostics Related to reporting of diagnostics. labels Jul 11, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Jul 11, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Comment on lines 109 to 112
// The schema says this is optional, so we may be able to skip the preview check.
if range.is_none() && !config.preview {
range = Some(RdjsonRange::default());
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main preview check I think we could skip, based on this definition of Location:

https://github.com/reviewdog/reviewdog/blob/320a8e73a94a09248044314d8ca326a6cd710692/proto/rdf/jsonschema/DiagnosticResult.json#L126-L141

As I mention in the module comment, only the description field says "Optional," not the type field.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest using a schema validator to validate our understanding, e.g. https://www.jsonschemavalidator.net/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A null range is invalid according to the validator, so this should just be unwrap_or_default irrespective of preview.

Actually, completely omitting it from the JSON also passes the validator, so I guess another option is skipping serialization if it's None.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, we should skip it then (which should be easy enough with serde)

message: diagnostic.body(),
location: RdjsonLocation { path, range },
code: RdjsonCode {
value: diagnostic.secondary_code(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is actually not optional in the schema, but this is consistent with main. I guess we may need to unwrap_or_default here. The same might be true for url. Both are currently null for syntax errors:

"code": {
"url": null,
"value": null
},

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion here is to use the lint id if the secondary code is missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, updated! The validator is also complaining about the URL. I guess I'll just put the CARGO_PKG_HOMEPAGE if there's no rule-specific page available.

@ntBre ntBre marked this pull request as ready for review July 11, 2025 21:31
name: "ruff",
url: env!("CARGO_PKG_HOMEPAGE"),
},
severity: "warning",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also consistent with main. We could use our real severity here, but it would change the default from warning to error for existing Ruff diagnostics.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest making this change under preview

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that this is a single severity for the whole collection. I guess we can use the max severity in preview.

There's also a severity field on individual diagnostics that we aren't using. I'll try that in the schema validator too.

@MichaReiser
Copy link
Member

@ntBre is this ready for re-review or are there more changes that you plan on making (I have no particular reason to believe that there's anything that needs changing. I just want to avoid taking a look if you plan on making changes to due the schema discussion we had)

@ntBre
Copy link
Contributor Author

ntBre commented Jul 13, 2025

Yes it's ready again, I just added your suggestion about skipping the range instead of using a default.

Comment on lines 112 to 115
// The schema does _not_ say this is optional, so always unwrap to a default value.
let path = span
.map(|span| span.file().path(resolver))
.unwrap_or_default();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

location is also an additional property. Can you verify if the JSON is valid if you omit the location?

If it can be omitted, we then shouldn't call back to unwrap_or_default and instead omit the location when a file is missing (a diagnostic should never have just a range because a range without a file is not very useful)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's valid to omit the location. I'll do that instead.

.map(|span| span.file().path(resolver))
.unwrap_or_default();

let severity = if config.preview {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep setting WARNING for all diagnostics until we officially support a WARNING severity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We weren't setting the per-diagnostic severity at all before. Would it be better to go back to that, or to set them all to WARNING?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably remove it for now. We can add the severity when introducing a warning severity to ruff

Comment on lines 130 to 132
url: diagnostic
.to_ruff_url()
.unwrap_or_else(|| env!("CARGO_PKG_HOMEPAGE").to_string()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm inclined to go with what they have in ther protobuf implementation where, as far as I can understand, it's valid to omit the url property when empty

https://github.com/reviewdog/reviewdog/blob/294118b12c6b85c28d21deb0458703cc7a878608/proto/rdf/reviewdog.pb.go#L660-L669

I'm inclined to go with that for now (the same with the range on location)

Even their own tests don't specify a url: https://github.com/reviewdog/reviewdog/blob/master/parser/rdjson_test.go

Comment on lines +139 to +142
fn rdjson_suggestions<'a>(
edits: &'a [Edit],
source_code: Option<SourceCode>,
) -> Vec<RdjsonSuggestion<'a>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add an assertion to Diagnostic::set_fix instead that asserts that it has an associated source files. Edits without a source file aren't very useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a debug_assert! and then updated some tests that were actually setting fixes on diagnostics without files. Happy to upgrade to assert if you prefer.

Comment on lines 225 to 226
// TODO(brent) this can be required after it's out of preview.
#[serde(skip_serializing_if = "Option::is_none")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the benefit is of setting the severity at the file level. I'm also inclined to just keep this None until we add a WARNING severity. Adding severities feels like an unrelated change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, I'll revert the per-diagnostic severity and just keep WARNING as the global severity. I agree that this feels unrelated to the rest of the refactor.

@ntBre ntBre marked this pull request as draft July 14, 2025 12:10
@ntBre ntBre marked this pull request as ready for review July 14, 2025 14:38
@ntBre
Copy link
Contributor Author

ntBre commented Jul 14, 2025

I think this should be ready for another look. The main thing I'm unsure about is the debug_assert in Diagnostic::set_fix combined with the runtime unwrap in rdjson_suggestions. Should they both be debug_assert or both full assert/unwrap?

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you.

],
"message": null
},
"fix": null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any other test for edits? If not, I think it's worth preserving testing fixes in some test or another.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the output and notebook_output unit tests and the CLI integration tests all have fixes/edits.

Comment on lines 76 to 78
let location = span
.map(|span| span.file().path(resolver))
.map(|path| RdjsonLocation { path, range });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could we use source_code.name() here? It makes it clearer that range depends on the same value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the risk of missing something obvious here, I don't see a SourceCode::name method. I think the SourceFile has the name but not the SourceCode. The Span seems like the common ancestor for both range and location, unless I factored out a shared span.file() call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it's SourceCode and not SourceFile...

I would rewrite this to

	let source_file = span.map(|span| {
        let file = span.file();
        (file.path(resolver), file.diagnostic_source(resolver))
    });

    let location = source_file.as_ref().map(|(path, source)| {
        let range = diagnostic.range().map(|range| {
            let source_code = source.as_source_code();
            let start = source_code.line_column(range.start());
            let end = source_code.line_column(range.end());
            RdjsonRange::new(start, end)
        });

        RdjsonLocation { path, range }
    });

    let edits = diagnostic.fix().map(Fix::edits).unwrap_or_default();

    RdjsonDiagnostic {
        message: diagnostic.body(),
        location,
        code: RdjsonCode {
            value: diagnostic
                .secondary_code()
                .map_or_else(|| diagnostic.name(), |code| code.as_str()),
            url: diagnostic.to_ruff_url(),
        },
        suggestions: rdjson_suggestions(edits, source_file.map(|(_, source)| source)),
    }

It makes it clearer that range can only be set when span isn't None.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I like this better than the version in this PR with many separate map calls and also better than the mutable version in #19340. I'll try to use this pattern going forward.

Comment on lines 99 to 102
.map(|edit| {
// Safety: we assert that diagnostics with fixes have an associated source file in
// `Diagnostic::set_fix`.
let source_code = source_code.as_ref().unwrap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: You could make this code more error resilient by skipping all edits if source_code is empty (but use a debug_assert to make it fail if that happens).

It has the benefit that Ruff doesn't panic if we ever fail to set the source file

ntBre and others added 3 commits July 14, 2025 12:33
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Micha Reiser <micha@reiser.io>
@ntBre ntBre enabled auto-merge (squash) July 15, 2025 12:36
@ntBre ntBre merged commit e9b0c33 into main Jul 15, 2025
34 checks passed
@ntBre ntBre deleted the brent/rdjson branch July 15, 2025 12:39
dcreager added a commit that referenced this pull request Jul 15, 2025
* main:
  [`pylint`] Extend invalid string character rules to include t-strings (#19355)
  Make TC010 docs example more realistic (#19356)
  Move RDJSON rendering to `ruff_db` (#19293)
  [`flake8-use-pathlib`] Skip single dots for `invalid-pathlib-with-suffix` (`PTH210`) on versions >= 3.14 (#19331)
  [`ruff`] Allow `strict` kwarg when checking for `starmap-zip` (`RUF058`) in Python 3.14+ (#19333)
  [ty] Reduce false positives for `TypedDict` types (#19354)
  [ty] Remove `ConnectionInitializer` (#19353)
  [ty] Use `Type::string_literal()` more (#19352)
  [ty] Add ecosystem-report workflow (#19349)
  [ty] Make use of salsa `Lookup` when interning values (#19347)
  [ty] Sync vendored typeshed stubs (#19345)
  [`pylint`] Make example error out-of-the-box (`PLE2502`) (#19272)
  [`pydoclint`] Fix `SyntaxError` from fixes with line continuations (`D201`, `D202`) (#19246)
dcreager added a commit that referenced this pull request Jul 15, 2025
* dcreager/merge-arguments:
  add types iterator
  add asserting constructor
  debug assert lengths
  remove unused From
  use FromIterator
  [`pylint`] Extend invalid string character rules to include t-strings (#19355)
  Make TC010 docs example more realistic (#19356)
  Move RDJSON rendering to `ruff_db` (#19293)
  [`flake8-use-pathlib`] Skip single dots for `invalid-pathlib-with-suffix` (`PTH210`) on versions >= 3.14 (#19331)
  [`ruff`] Allow `strict` kwarg when checking for `starmap-zip` (`RUF058`) in Python 3.14+ (#19333)
  [ty] Reduce false positives for `TypedDict` types (#19354)
  [ty] Remove `ConnectionInitializer` (#19353)
  [ty] Use `Type::string_literal()` more (#19352)
  [ty] Add ecosystem-report workflow (#19349)
  [ty] Make use of salsa `Lookup` when interning values (#19347)
  [ty] Sync vendored typeshed stubs (#19345)
  [`pylint`] Make example error out-of-the-box (`PLE2502`) (#19272)
  [`pydoclint`] Fix `SyntaxError` from fixes with line continuations (`D201`, `D202`) (#19246)
UnboundVariable pushed a commit to UnboundVariable/ruff that referenced this pull request Jul 15, 2025
…finition

* 'main' of https://github.com/astral-sh/ruff: (39 commits)
  [ty] Sync vendored typeshed stubs (astral-sh#19368)
  Fix typeshed-sync workflow (astral-sh#19367)
  Rework typeshed-sync workflow to also add docstrings for Windows- and MacOS-specific APIs (astral-sh#19360)
  [ty] Allow `-qq` for silent output mode (astral-sh#19366)
  [ty] Allow `-q` short alias for `--quiet` (astral-sh#19364)
  Add shellcheck to pre-commit (astral-sh#19361)
  distinguish references from definitions in `infer_nonlocal`
  [`pycodestyle`] Handle brace escapes for t-strings in logical lines (astral-sh#19358)
  [ty] Combine CallArguments and CallArgumentTypes (astral-sh#19337)
  Move Pylint rendering to `ruff_db` (astral-sh#19340)
  [`pylint`] Extend invalid string character rules to include t-strings (astral-sh#19355)
  Make TC010 docs example more realistic (astral-sh#19356)
  Move RDJSON rendering to `ruff_db` (astral-sh#19293)
  [`flake8-use-pathlib`] Skip single dots for `invalid-pathlib-with-suffix` (`PTH210`) on versions >= 3.14 (astral-sh#19331)
  [`ruff`] Allow `strict` kwarg when checking for `starmap-zip` (`RUF058`) in Python 3.14+ (astral-sh#19333)
  [ty] Reduce false positives for `TypedDict` types (astral-sh#19354)
  [ty] Remove `ConnectionInitializer` (astral-sh#19353)
  [ty] Use `Type::string_literal()` more (astral-sh#19352)
  [ty] Add ecosystem-report workflow (astral-sh#19349)
  [ty] Make use of salsa `Lookup` when interning values (astral-sh#19347)
  ...

# Conflicts:
#	crates/ty_ide/src/goto.rs
#	crates/ty_server/src/server.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

diagnostics Related to reporting of diagnostics. internal An internal refactor or improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants