Merge 0.15.2#1
Conversation
…uration files (astral-sh#24402) ## Summary If a user specifies an unsupported value in `environment.python-version`, we need to reject it, like we do on the CLI.
## Summary Generally straightforward given that we support `type(...)`; however, I think the base class validation can be a bit looser, since `types.new_class` does proper metaclass resolution. Closes astral-sh/ty#2399.
…-sh#24410) ## Summary It looks like this code was special-casing `TokenKind::String`, but missed the non-`TokenKind::String` string-like tokens (like for f-strings). Closes astral-sh#24409.
## Summary Addresses: astral-sh#23144 (comment).
…bles (astral-sh#24281) ## Summary We allow private attribute access _within_ the implementing class (e.g., `self._foo`), but historically, this was implemented by matching on `self`, `cls`, and `mcs`. So, e.g., if you used a self variable name other than `self`, we'd still flag accesses. With this PR, we now model self correctly by detecting it as the "first argument to a method", removing those false positives. For now, however, we _also_ keep the blanket exemption for `self`, `cls`, and `mcs` in stable, since that's removing what are arguably false negatives and thus introducing new diagnostics. Closes astral-sh#24275.
## Summary Before Python 3.12, a replacement field in an f-string can span multiple lines only if the outer f-string is triple-quoted. This was relaxed in Python 3.12, but we weren't rejecting these as syntactically invalid on earlier versions. Closes astral-sh#24348.
…l-sh#24377) ## Summary See: astral-sh#24355 (comment). Prior to Python 3.12, we need to avoid emitting formatted expressions that span multiple lines in non-triple quoted f-strings.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…rs (astral-sh#24451) ## Summary Small optimization: only determinate reachability for "bad" declarations, so that we do the cheap check (is it "bad"?) before the expensive check ("is it reachable?"). CodSpeed shows a 2% improvement on Pydantic and 1% on some other projects like Pandas and SymPy.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Copied from the pattern added to uv in astral-sh/uv#17388 This will allow the required checks for pull requests to be mutated 1. Without breaking all open pull requests every time we need to change them 2. Without admin permissions to the repository After this has been merged for a period of time, we'll update the required checks in the repository to just be this one job
## Summary Hide "rule xyz is enabled …" hints by default, unless in `--verbose` mode ## Test Plan - Updated snapshot tests - Tested in the CLI
Hi, this fixes astral-sh/ty#2854 ## Summary Compare class literals instead of class bases for the purposes of skipping the MRO until the pivot class. Also skip `BoundSuperType` when looking up definitions since we probably do not want to look up definitions on the `super` class itself. ## Test Plan Added snapshot tests --------- Co-authored-by: Douglas Creager <dcreager@dcreager.net>
…rs (astral-sh#24383) The `ConstraintSet::solutions` method returns a (set of) solutions for a constraint set — assignments of specific types to each typevar in question. astral-sh#23848 introduced two variants of this method. One of them (`solutions_with_inferable`) would take in the set of `inferable` typevars. This was used in a cycle check at the beginning of the method, to make sure that we only considered the typevars we're actually solving for when detecting a cyclic constraint set. More importantly, it was also used to limit the result, so that we would only get solutions for ther inferable typevars (i.e., the ones that we're using the constraint set to solve for). A cleaner approach is to use _extensional quantification_ to _remove_ the non-inferable typevars from the constraint set before calculating solutions. We already had this available as a lower-level TDD method (`abstract_one_inner`), which lets us provide an arbitrary `should_remove` callback to determine which constraints to keep and which to remove. We just need to add a new public API method that provides a `should_remove` callback that keeps only the constraints involving inferable typevars. Given this change we can actually remove the cyclic checks completely, since `SequentMap` and `PathAssignments` will already bottom out if they encounter a cycle in the constraints. (Specifically, while we're walking TDD paths, `PathAssignments` will only add constraints that aren't already present in the current path.)
… workspace (astral-sh#24775) ## Summary Distribute watched file change events generated by the LSP client to all dbs. This simplifies the filtering logic to be handled by each db, and also fixes venvs which are outside the workspace (which can happen for symlinked environments). ## Test Plan Neovim, and a custom test harness (let me know if there is a way to test this in ty itself, Micha made it sound like it wasn't so easy.
…l-sh#24789) Since it may take a little more time to support user-configured severities, it makes sense to revert this change for now as it is disruptive to users and there is no workaround. (I don't think it's necessary to change the preview behavior in the CLI since both the displayed color and the exit-code behavior is unchanged.) Closes astral-sh#24069
astral-sh#24703) ## Summary We already support `**` unpacking of `TypedDict` in named `TypedDict` constructors; this PR extends it to literals annotated as `TypedDict`, as in: ```python from typing import TypedDict class MyTypedDict1(TypedDict): aaa: int bbb: int class MyTypedDict2(TypedDict): aaa: int bbb: int ccc: int d1: MyTypedDict1 = { "aaa": 1, "bbb": 2, } d2: MyTypedDict2 = { **d1, "ccc": 3, } ``` Closes astral-sh/ty#1493
Co-authored-by: ShipItAndPray <noreply@users.noreply.github.com>
Closes astral-sh/ty#3305 <img width="611" height="392" alt="Screenshot 2026-04-22 at 17 32 50" src="https://github.com/user-attachments/assets/8470a87f-efc5-4998-8f25-c48befea368e" />
astral-sh#24153) Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
## Summary Closes astral-sh/ty#3304.
…ays returns the same value (astral-sh#24749) Co-authored-by: Abhay <abhayjnayakpro@gmail.com>
…al-sh#23579) ## Summary Adds a new rule `AIR004` that detects `@task.branch` decorated functions that could be replaced with `@task.short_circuit`. In Airflow, [`@task.branch`](https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/dags.html#branching) selects which downstream tasks to run by returning a list of task IDs (or an empty list to skip all). When the function has at least two `return` statements and exactly one of them returns a non-empty list, it is effectively acting as a boolean short-circuit (i.e. either run one specific set of downstream tasks or skip them all). In that case, [`@task.short_circuit`](https://www.astronomer.io/docs/learn/airflow-branch-operator#taskshort_circuit-shortcircuitoperator) is a simpler and more readable alternative that returns `True`/`False` instead. ```python # Before (AIR004) @task.branch def my_task(): if condition: return ["my_downstream_task"] return [] # After @task.short_circuit def my_task(): return condition ``` ### Implementation details - Resolves the `@task.branch` decorator via the semantic model (`airflow.decorators.task` + `.branch` attribute), handling both `@task.branch` and `@task.branch()` call forms via `map_callable`. - Uses `ReturnStatementVisitor` to collect all `return` statements recursively (including those inside nested `if`/`else`/`for`/`while` blocks). - Flags the function when: `len(returns) >= 2` and exactly one return has a non-empty list value. ### What it does NOT flag - Functions with multiple non-empty list returns (genuine branching logic). - Functions with all-empty returns (no downstream tasks selected at all). - Functions with only a single return statement. - Functions not decorated with `@task.branch`. - Functions returning non-list values (strings, `None`, etc.). ## Test Plan <!-- How was it tested? --> Added snapshot tests in `AIR004.py` covering both violation and non-violation cases: - two returns with one non-empty list - three returns with one non-empty list - nested returns - multiple non-empty returns - all-empty returns - single return - undecorated functions - `@task.short_circuit` decorated functions
## Summary Add error context for `TypedDict` to `TypedDict` assignments ## Test Plan Adapted and new Markdown tests.
## Summary This implementation is basically the dual of what we do for unions (with the assignability direction flipped). ## Test Plan Updated mdtests.
…l-sh#24317) Improves astral-sh#22633 to infer the use of lambda parameters in a lambda body with type context, e.g., ```py x: Callable[[str], str] = lambda x: reveal_type(x) # revealed: str reveal_type(x) # revealed: (x: str) -> str ``` Unlike other definitions, lambda parameter types cannot be determined purely syntactically in semantic indexing. Instead, they depend on the inferred type of the lambda to access its parameter types. Unfortunately, this makes lambda inference cyclic, as the body of the lambda depends on the outer lambda type, and there is no obvious way of splitting out inference of the lambda parameter types from its return type. To avoid initiating cycles on the entire scope containing the lambda, this PR introduces a new inference query — statement-level inference. Statements are a minimal unit of code that encapsulate any internal type context. This makes them very useful to infer a given sub-expression "naturally" without having to provide any external type context. There are other places where we currently rely on scope-level inference for this purpose (e.g., see `infer_complete_scope_types`, the current implementation of astral-sh#23761, and the discussion in astral-sh/ty#3124). Note that statement-level inference is not perfectly fine-grained, e.g., the test expression of an `if` statement does not require external type context and is independent from its body, so statement-level inference may lead to unnecessarily large cycles, but having the unit of code being generalized to an AST structure allows us to avoid the need for such special cases, but this can always change in the future. Additionally, many statements are simply wrappers around definitions or standalone expressions, so we can avoid extra salsa allocations in the common case.
## Summary Per astral-sh#19599 (comment), maturin now supports PEP 639 fully. Consequently we should drop the license classifier, as it's legally ambiguous. See also astral-sh/uv#19130 for where we're doing the same for uv. ## Test Plan NFC. Signed-off-by: William Woodruff <william@astral.sh>
…ion/intersection TypedDicts (astral-sh#24693) ## Summary Closes astral-sh/ty#3271 When subscripting a union or intersection of TypedDicts with an invalid key, the concise diagnostic message now includes the source expression and its full inferred type. This lets programmatic consumers of ty's output distinguish between: 1. Key invalid on one branch of a union — may be intentional (e.g. probing for a key that only exists on some branches) 2. Key truly invalid on a single-typed variable — likely a bug ### Before ``` file.py:19:14: error[invalid-key] Unknown key "foo" for TypedDict `DictB` file.py:22:14: error[invalid-key] Unknown key "bar" for TypedDict `DictA` ``` Both errors look identical in structure — there is no way to tell from concise output that line 19 involves a union type while line 22 does not. ### After ``` file.py:19:14: error[invalid-key] Unknown key "foo" for TypedDict `DictB` (`resp` has type `DictA | DictB`) file.py:22:14: error[invalid-key] Unknown key "bar" for TypedDict `DictA` ``` The source expression name is used (e.g. `resp`, `obj.data`), making the message immediately understandable. The full (non-concise) diagnostic output is unchanged — it already showed the union/intersection context via a secondary annotation. ## Test Plan - Updated 3 existing mdtest assertions in `typed_dict.md` to verify the new concise message format for union and intersection cases - All 330 tests in `ty_python_semantic` pass - Clippy clean - Manually verified concise output on the reproduction from the linked issue
Follow-up to astral-sh#23404 Add support for `#ruff:file-ignore[code]` style file-level suppressions as own-line comments at global module scope. The range covered by these suppressions is always the entirety of the file: ```py # ruff:file-ignore[ARG001] def foo( arg1, arg2, ): pass def bar( arg1, arg2, ): pass ``` This currently requires having preview mode enabled. Without preview mode, the comments are parsed and processed, but not materialized into active suppressions at runtime.
…nd errors from implicit dunder calls against unions. (astral-sh#24676) <!-- Thank you for contributing to Ruff/ty! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? (Please prefix with `[ty]` for ty pull requests.) - Does this pull request include references to any relevant issues? - Does this PR follow our AI policy (https://github.com/astral-sh/.github/blob/main/AI_POLICY.md)? --> ## Summary <!-- What's the purpose of the change? What does it do, and why? --> Building on astral-sh#24662, and to resolve astral-sh/ty#940, this adds more "info" sub-diagnostics to possibly unbound method error messages that occur when a dunder is called implicitly against a union type. The implicit dunder calls covered here are `__iter__`, `__await__` and `__mro_entries__`. ## Test Plan Please see new/updated mdtests and related snapshots. <!-- How was it tested? -->
📝 WalkthroughWalkthroughVersion bump to Ruff 0.15.12 with new Airflow rules (AIR004, AIR201), mdtest refactoring for snapshot-based testing, GitHub workflow and dependency updates, extensive test fixtures for rule coverage, and cache permission fixes. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B909.py (1)
123-315:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate the fixture header to avoid stale expected-line metadata.
With the new added scenarios, the top docstring’s explicit line list is no longer accurate (see Line 3). That makes fixture expectations harder to trust during future edits.
Suggested update
""" Should emit: -B909 - on lines 11, 25, 26, 40, 46 +B909 diagnostics in the marked "should error" cases below. """🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B909.py` around lines 123 - 315, The fixture's top docstring contains stale explicit expected-line metadata (it no longer matches the added tests like pass_try_finally_return, fail_nested_return_bypasses_else, fail_nested_raise_bypasses_else, etc.); open the file header and update the docstring to remove or regenerate the hard-coded line list so expectations are not tied to fixed line numbers (e.g., replace the explicit line list with a short description or recalculated ranges), then run the test fixture generator or adjust the header to reflect the new scenarios so tests referencing the header no longer fail due to outdated line metadata.
🧹 Nitpick comments (2)
crates/ruff_linter/src/rules/airflow/rules/task_branch_as_short_circuit.rs (1)
179-200: 💤 Low valueConsider whether non-list return values should be validated.
The
could_be_short_circuitfunction counts returns with non-empty lists and triggers when exactly one exists. However, it doesn't verify that the other returns are actually empty lists,None, or barereturnstatements.For example, a function with:
if condition: return ["task"] return 42 # Not an empty listWould have
non_empty_list_count == 1and trigger AIR004, even thoughreturn 42isn't a valid short-circuit pattern.This may be acceptable since such code is likely invalid for a branch function anyway, but consider whether you want to be more defensive here.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/ruff_linter/src/rules/airflow/rules/task_branch_as_short_circuit.rs` around lines 179 - 200, The current could_be_short_circuit function only counts non-empty list returns but doesn't validate that all other returns are acceptable short-circuit forms; update could_be_short_circuit (and/or use ReturnStatementVisitor/returns) to ensure that when exactly one non-empty list return exists, every other return's value is either an empty list, a None literal, or no value (bare return). Implement this by iterating returns: identify the single non-empty list return and verify every other ret.value.as_deref() matches Expr::List with empty elts OR Expr::Constant/NameConstant None OR is None (bare return); only return true if all others match those allowed patterns..github/workflows/publish-versions.yml (1)
33-35: Explicitly pin theuvCLI version in setup-uv for reproducible releases.The
setup-uvaction currently uses the defaultuvversion (falling back to latest if no version is configured). This release workflow should explicitly set theversioninput to ensure consistent tool behavior across runs:- name: "Install uv" uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: version: "<version>" # Pin to a specific uv CLI releaseWithout an explicit pin, publishing behavior could drift if a new
uvversion is released upstream.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/publish-versions.yml around lines 33 - 35, The workflow step named "Install uv" currently uses astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b without an explicit version input; update that step (the "Install uv" job step using the setup-uv action) to include a with: version: "<version>" entry to pin the uv CLI to a specific release (replace "<version>" with the desired semver or tag) so the publish-versions.yml run is reproducible and not subject to upstream uv changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/ty-ecosystem-analyzer.yaml:
- Around line 102-108: Set the matrix job analyze-shards to not short-circuit
sibling jobs by adding strategy.fail-fast: false and ensure the consumer job
generate-report tolerates missing shard artifacts: change its dependency to
still run even if some analyze-shards legs fail (use if: always() or depend on
the analyze-shards job but use actions/download-artifact with continue-on-error
or conditional checks). Update the download/merge steps in generate-report so
they use if: always() or continue-on-error and handle absent artifacts
gracefully (e.g., skip missing shard files and produce a partial merged report),
referencing the analyze-shards job, strategy.fail-fast, generate-report job, and
the download/merge steps when making changes.
In `@crates/mdtest/src/assertion.rs`:
- Around line 77-115: The current logic advances stacked own-line assertions by
one line (line_number += 1), but UnparsedAssertionsIter may skip blank lines and
non-assertion comments so the collected own-line comments must attach to the
next actual code line; change the "if only_own_line { line_number =
line_number.saturating_add(1); }" behavior to find and set line_number to the
next non-empty, non-comment line (i.e., scan forward from the current line using
file_index.line_index and the source buffer or by peeking/consuming
UnparsedAssertionsIter) so the collector entries created from
CommentRanges::is_own_line(...) attach to the next real code line instead of
blindly +1; keep use of file_assertions.next_if and UnparsedAssertionsIter
semantics when skipping assertions/comments.
In `@crates/mdtest/src/lib.rs`:
- Around line 87-91: In the OutputFormat::GitHub branch (the println! calls that
emit GitHub Actions annotations) percent-encode the annotation property values
and messages before interpolation: apply the property encoding (%→%25, :→%3A,
,→%2C, \r→%0D, \n→%0A) to file, line (if used as a string), and fixture_path,
and apply the message encoding (%→%25, \r→%0D, \n→%0A) to failure.message() and
render_diff() output; implement helper(s) (e.g., encode_github_prop and
encode_github_msg) and call them where the println! for OutputFormat::GitHub
formats file, line, fixture_path, or message so annotations are safe for GitHub
Actions parsing.
In `@crates/mdtest/src/matcher.rs`:
- Around line 23-30: The Failure type and its internal data are not accessible
to downstream callers because Failure (and/or its fields) are private and
existing getters are pub(super); make Failure inspectable by making the Failure
struct public (pub struct Failure) and either expose its fields as pub or add
public accessor methods on Failure (e.g., pub fn message(&self) -> &str) so
callers using match_file and FailuresByLine::iter() can read failure details;
update any related types (LineFailures) visibility as needed so the iterator
returned by FailuresByLine::iter() yields inspectable Failure values.
- Around line 165-169: Replace the non-deterministic sort_unstable_by call in
the failures.is_empty() branch with a deterministic sort that uses
rendering_sort_key(db) as the primary key and a stable secondary tie-breaker
(e.g., diagnostic span start/end, the rendered message text, or a stable
diagnostic id) so two diagnostics with equal rendering_sort_key cannot flip
order; update the sort call on snapshot_diagnostics (the same variable and block
where rendering_sort_key(db) is used) to compare the primary key then cmp the
chosen secondary field to ensure fully deterministic ordering.
In `@crates/mdtest/src/parser.rs`:
- Around line 942-961: The snapshot-binding logic currently uses
self.current_section_files.last() and
file.python_code_blocks.last_mut().unwrap(), which allows intervening prose to
leave the previous Python block as a target; introduce a dedicated
pending_snapshot_target field on the parser state (e.g., Option<(file_index,
block_index)>) that is set when parsing a checkable Python code block and
cleared whenever any non-code content, new section, or a different block type is
parsed; change the snapshot handler to use pending_snapshot_target (and bail
with a clear error if None) instead of current_section_files.last(), and ensure
code that consumes the python code block updates/clears pending_snapshot_target
as appropriate (update references: current_section_files, files,
python_code_blocks, and the snapshot handling site).
- Around line 961-969: The error message computes the markdown line for the
Python code block using code_block.embedded_start_offset(), which is relative to
the concatenated embedded file and yields the wrong markdown line; update the
calculation to use the code block’s markdown fence offset (the offset that
refers into the original markdown source) when calling line_number, i.e. replace
the use of code_block.embedded_start_offset() with the code-block
method/property that returns the markdown fence/markdown-start offset so
code_block_start is computed against self.source; keep the other offsets
(offsets.start() and existing_block.range.start()) as-is.
In `@crates/ruff_db/src/diagnostic/mod.rs`:
- Around line 1374-1378: Update the doc comment for the merge_window field to
state that merging is applied to rendered snippets (not raw annotations) and
that the effective merge gap is clamped to at least the configured context
(i.e., effective_window = max(merge_window, context)), so callers using
merge_window(0) with non‑zero context will still merge according to context;
apply the same clarification to the other doc block around the merge semantics
(the second comment at 1437-1446) and reference the fields named merge_window
and context in the text.
---
Outside diff comments:
In `@crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B909.py`:
- Around line 123-315: The fixture's top docstring contains stale explicit
expected-line metadata (it no longer matches the added tests like
pass_try_finally_return, fail_nested_return_bypasses_else,
fail_nested_raise_bypasses_else, etc.); open the file header and update the
docstring to remove or regenerate the hard-coded line list so expectations are
not tied to fixed line numbers (e.g., replace the explicit line list with a
short description or recalculated ranges), then run the test fixture generator
or adjust the header to reflect the new scenarios so tests referencing the
header no longer fail due to outdated line metadata.
---
Nitpick comments:
In @.github/workflows/publish-versions.yml:
- Around line 33-35: The workflow step named "Install uv" currently uses
astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b without an explicit
version input; update that step (the "Install uv" job step using the setup-uv
action) to include a with: version: "<version>" entry to pin the uv CLI to a
specific release (replace "<version>" with the desired semver or tag) so the
publish-versions.yml run is reproducible and not subject to upstream uv changes.
In `@crates/ruff_linter/src/rules/airflow/rules/task_branch_as_short_circuit.rs`:
- Around line 179-200: The current could_be_short_circuit function only counts
non-empty list returns but doesn't validate that all other returns are
acceptable short-circuit forms; update could_be_short_circuit (and/or use
ReturnStatementVisitor/returns) to ensure that when exactly one non-empty list
return exists, every other return's value is either an empty list, a None
literal, or no value (bare return). Implement this by iterating returns:
identify the single non-empty list return and verify every other
ret.value.as_deref() matches Expr::List with empty elts OR
Expr::Constant/NameConstant None OR is None (bare return); only return true if
all others match those allowed patterns.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c550a94f-3b8a-4288-89d1-140cff306dbc
⛔ Files ignored due to path filters (175)
Cargo.lockis excluded by!**/*.lockcrates/ruff/tests/cli/snapshots/cli__format__output_format_full.snapis excluded by!**/*.snapcrates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snapis excluded by!**/*.snapcrates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snapis excluded by!**/*.snapcrates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snapis excluded by!**/*.snapcrates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snapis excluded by!**/*.snapcrates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snapis excluded by!**/*.snapcrates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snapis excluded by!**/*.snapcrates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snapis excluded by!**/*.snapcrates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snapis excluded by!**/*.snapcrates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snapis excluded by!**/*.snapcrates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snapis excluded by!**/*.snapcrates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snapis excluded by!**/*.snapcrates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__full__tests__notebook_output_with_diff.snapis excluded by!**/*.snapcrates/ruff_db/src/diagnostic/render/snapshots/ruff_db__diagnostic__render__full__tests__notebook_output_with_diff_spanning_cells.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR004_AIR004.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR004_AIR004_sdk.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR201_AIR201.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_args.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_context.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_decorator.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names_fix.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_provider_names_fix.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_amazon.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_celery.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_common_sql.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_daskexecutor.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_druid.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_fab.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hdfs.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hive.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_http.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_jdbc.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_kubernetes.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_mysql.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_oracle.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_papermill.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_pig.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_postgres.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_presto.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_samba.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_slack.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_smtp.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_sqlite.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_standard.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_zendesk.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_args.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR321_AIR321_names.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/eradicate/snapshots/ruff_linter__rules__eradicate__tests__ERA001_ERA001.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__deferred_annotations_diff_fast-api-redundant-response-model_FAST001.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__deferred_annotations_diff_fast-api-unused-path-parameter_FAST003.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_0.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_0.py_py38.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_1.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_1.py_py38.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_2.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-non-annotated-dependency_FAST002_2.py_py38.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__auto_return_type.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__auto_return_type_py38.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__defaults.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__ignore_fully_untyped.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__mypy_init_return.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__shadowed_builtins.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__simple_magic_methods.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_annotations/snapshots/ruff_linter__rules__flake8_annotations__tests__suppress_none_returning.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC105_ASYNC105.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC109_0.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC109_ASYNC109_0.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC115_ASYNC115.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC116_ASYNC116.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S103_S103.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S408_S408_type_checking.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S103_S103.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B004_B004.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_1.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_2.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_3.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_4.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_5.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_6.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_7.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_8.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_9.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B007_B007.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B009_B009_B010.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B010_B009_B010.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B011_B011.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B013_B013.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B014_B014.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B033_B033.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B043_B043.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B905.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B909_B909.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B912_B912.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__extend_immutable_calls_arg_annotation.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_1.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_2.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_3.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_4.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_5.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_6.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_7.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_8.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_9.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_B008.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C400_C400.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C401_C401.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C402_C402.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C403_C403.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C404_C404.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C405_C405.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py_allow_dict_calls_with_keyword_arguments.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C409_C409.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C410_C410.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C411_C411.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C413_C413.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C414_C414.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C416_C416.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C417_C417.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C417_C417_1.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C418_C418.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420_1.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420_2.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C409_C409.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C419_C419_1.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__custom.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__defaults.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__string_exception.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE004_4.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__edge_case.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import_many.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing_as.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC003_ISC.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__same_name.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__tricky.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG001_LOG001.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG002_LOG002.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG004_LOG004_0.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG004_LOG004_1.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG009_LOG009.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG014_LOG014_0.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_logging/snapshots/ruff_linter__rules__flake8_logging__tests__LOG014_LOG014_1.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G010.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004_implicit_concat.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE790_PIE790.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE794_PIE794.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE800_PIE800.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE804_PIE804.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE807_PIE807.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE808_PIE808.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE810_PIE810.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_print/snapshots/ruff_linter__rules__flake8_print__tests__T201_T201.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_print/snapshots/ruff_linter__rules__flake8_print__tests__T203_T203.py.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI009_PYI009.pyi.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI010_PYI010.pyi.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI011_PYI011.pyi.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI012_PYI012.pyi.snapis excluded by!**/*.snapcrates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI013_PYI013.py.snapis excluded by!**/*.snap
📒 Files selected for processing (125)
.git-blame-ignore-revs.github/CODEOWNERS.github/pr-assignee-pools.toml.github/workflows/build-binaries.yml.github/workflows/build-docker.yml.github/workflows/build-wasm.yml.github/workflows/ci.yaml.github/workflows/daily_fuzz.yaml.github/workflows/memory_report.yaml.github/workflows/notify-dependents.yml.github/workflows/publish-mirror.yml.github/workflows/publish-pypi.yml.github/workflows/publish-versions.yml.github/workflows/release.yml.github/workflows/sync_typeshed.yaml.github/workflows/ty-ecosystem-analyzer.yaml.github/workflows/ty-ecosystem-report.yaml.github/workflows/typing_conformance.yaml.pre-commit-config.yamlCHANGELOG.mdCONTRIBUTING.mdCargo.tomlREADME.mdcrates/mdtest/Cargo.tomlcrates/mdtest/src/assertion.rscrates/mdtest/src/diagnostic.rscrates/mdtest/src/lib.rscrates/mdtest/src/matcher.rscrates/mdtest/src/parser.rscrates/ruff/Cargo.tomlcrates/ruff/src/cache.rscrates/ruff/src/commands/format.rscrates/ruff/tests/cli/format.rscrates/ruff/tests/cli/lint.rscrates/ruff/tests/integration_test.rscrates/ruff_annotate_snippets/Cargo.tomlcrates/ruff_annotate_snippets/src/renderer/display_list.rscrates/ruff_benchmark/Cargo.tomlcrates/ruff_benchmark/benches/parser.rscrates/ruff_benchmark/benches/ty.rscrates/ruff_benchmark/benches/ty_walltime.rscrates/ruff_benchmark/src/lib.rscrates/ruff_benchmark/src/real_world_projects.rscrates/ruff_cache/Cargo.tomlcrates/ruff_db/Cargo.tomlcrates/ruff_db/src/diagnostic/mod.rscrates/ruff_db/src/diagnostic/render.rscrates/ruff_db/src/diagnostic/render/concise.rscrates/ruff_db/src/diagnostic/render/full.rscrates/ruff_db/src/diagnostic/render/json.rscrates/ruff_db/src/parsed.rscrates/ruff_db/src/system.rscrates/ruff_db/src/system/memory_fs.rscrates/ruff_db/src/system/os.rscrates/ruff_db/src/system/test.rscrates/ruff_dev/src/format_dev.rscrates/ruff_dev/src/generate_options.rscrates/ruff_dev/src/generate_rules_table.rscrates/ruff_dev/src/generate_ty_cli_reference.rscrates/ruff_dev/src/generate_ty_options.rscrates/ruff_diagnostics/Cargo.tomlcrates/ruff_graph/Cargo.tomlcrates/ruff_index/src/slice.rscrates/ruff_linter/Cargo.tomlcrates/ruff_linter/resources/test/fixtures/airflow/AIR004.pycrates/ruff_linter/resources/test/fixtures/airflow/AIR004_sdk.pycrates/ruff_linter/resources/test/fixtures/airflow/AIR201.pycrates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC109_0.pycrates/ruff_linter/resources/test/fixtures/flake8_bandit/S103.pycrates/ruff_linter/resources/test/fixtures/flake8_bugbear/B909.pycrates/ruff_linter/resources/test/fixtures/flake8_errmsg/EM.pycrates/ruff_linter/resources/test/fixtures/flake8_logging/LOG004_0.pycrates/ruff_linter/resources/test/fixtures/flake8_self/SLF001_2.pycrates/ruff_linter/resources/test/fixtures/flake8_self/SLF001_custom_decorators.pycrates/ruff_linter/resources/test/fixtures/pycodestyle/E502.pycrates/ruff_linter/resources/test/fixtures/pylint/import_private_name/submodule/__main__.pycrates/ruff_linter/resources/test/fixtures/pyupgrade/UP012.pycrates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.pycrates/ruff_linter/resources/test/fixtures/ruff/RUF010.pycrates/ruff_linter/resources/test/fixtures/ruff/RUF019.pycrates/ruff_linter/resources/test/fixtures/ruff/RUF024.pycrates/ruff_linter/resources/test/fixtures/ruff/RUF029.pycrates/ruff_linter/resources/test/fixtures/ruff/RUF067/modules/__init__.pycrates/ruff_linter/resources/test/fixtures/ruff/RUF072.pycrates/ruff_linter/resources/test/fixtures/ruff/suppressions.pycrates/ruff_linter/src/checkers/ast/analyze/expression.rscrates/ruff_linter/src/checkers/ast/analyze/statement.rscrates/ruff_linter/src/checkers/ast/mod.rscrates/ruff_linter/src/checkers/noqa.rscrates/ruff_linter/src/codes.rscrates/ruff_linter/src/fix/edits.rscrates/ruff_linter/src/linter.rscrates/ruff_linter/src/preview.rscrates/ruff_linter/src/rules/airflow/helpers.rscrates/ruff_linter/src/rules/airflow/mod.rscrates/ruff_linter/src/rules/airflow/rules/mod.rscrates/ruff_linter/src/rules/airflow/rules/task_branch_as_short_circuit.rscrates/ruff_linter/src/rules/airflow/rules/variable_get_outside_task.rscrates/ruff_linter/src/rules/airflow/rules/xcom_pull_in_template_string.rscrates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rscrates/ruff_linter/src/rules/flake8_bandit/mod.rscrates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rscrates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_string.rscrates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rscrates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_bad_defaults.rscrates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rscrates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rscrates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rscrates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rscrates/ruff_linter/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rscrates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rscrates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rscrates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rscrates/ruff_linter/src/rules/flake8_django/helpers.rscrates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rscrates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rscrates/ruff_linter/src/rules/flake8_errmsg/rules/string_in_exception.rscrates/ruff_linter/src/rules/flake8_logging/helpers.rscrates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rscrates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rscrates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rscrates/ruff_linter/src/rules/flake8_pie/rules/duplicate_class_field_definition.rscrates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rscrates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rscrates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs
💤 Files with no reviewable changes (1)
- crates/ruff_db/Cargo.toml
| analyze-shards: | ||
| needs: [build-ty] | ||
| strategy: | ||
| matrix: | ||
| shard: [0, 1] | ||
| runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }} | ||
| timeout-minutes: 10 |
There was a problem hiding this comment.
Don't let one shard failure suppress the entire report.
With the current shape, GitHub's default matrix fail-fast can cancel the sibling shard, and generate-report will never start unless every analyze-shards leg succeeds. A single transient ecosystem failure therefore drops the merged diff artifacts and the abnormal-exit summary entirely.
Suggested direction
analyze-shards:
needs: [build-ty]
strategy:
+ fail-fast: false
matrix:
shard: [0, 1]
generate-report:
+ if: ${{ always() }}
name: Generate diagnostic diff report
needs: [analyze-shards]You'll also need the download/merge steps to tolerate missing shard artifacts so the report job can still emit partial output when one shard fails.
Also applies to: 160-163
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/ty-ecosystem-analyzer.yaml around lines 102 - 108, Set the
matrix job analyze-shards to not short-circuit sibling jobs by adding
strategy.fail-fast: false and ensure the consumer job generate-report tolerates
missing shard artifacts: change its dependency to still run even if some
analyze-shards legs fail (use if: always() or depend on the analyze-shards job
but use actions/download-artifact with continue-on-error or conditional checks).
Update the download/merge steps in generate-report so they use if: always() or
continue-on-error and handle absent artifacts gracefully (e.g., skip missing
shard files and produce a partial merged report), referencing the analyze-shards
job, strategy.fail-fast, generate-report job, and the download/merge steps when
making changes.
| if CommentRanges::is_own_line(ranged_assertion.start(), source) { | ||
| collector.push(ranged_assertion.into_comment()); | ||
| let mut only_own_line = true; | ||
|
|
||
| while let Some(ranged_assertion) = file_assertions.next_if(|next_pragma| { | ||
| let next_line_number = line_number.saturating_add(1); | ||
|
|
||
| if file_index.line_index(next_pragma.start()) == next_line_number { | ||
| line_number = next_line_number; | ||
| true | ||
| } else { | ||
| false | ||
| } | ||
| }) { | ||
| if !CommentRanges::is_own_line(ranged_assertion.start(), source) { | ||
| only_own_line = false; | ||
| } | ||
|
|
||
| fn is_own_line_comment(&self, ranged_assertion: &AssertionWithRange) -> bool { | ||
| CommentRanges::is_own_line(ranged_assertion.start(), self.source.as_str()) | ||
| } | ||
| } | ||
| collector.push(ranged_assertion.into_comment()); | ||
|
|
||
| impl<'a> IntoIterator for &'a InlineFileAssertions { | ||
| type Item = LineAssertions<'a>; | ||
| type IntoIter = LineAssertionsIterator<'a>; | ||
| // If we see an end-of-line comment, it has to be the end of the stack, | ||
| // otherwise we'd botch this case, attributing all three errors to the `bar` | ||
| // line: | ||
| // | ||
| // ```py | ||
| // # error: | ||
| // foo # error: | ||
| // bar # error: | ||
| // ``` | ||
| // | ||
| if !only_own_line { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| fn into_iter(self) -> Self::IntoIter { | ||
| Self::IntoIter { | ||
| file_assertions: self, | ||
| inner: AssertionWithRangeIterator { | ||
| file_assertions: self, | ||
| inner: self.comment_ranges.into_iter(), | ||
| if only_own_line { | ||
| // The collected comments apply to the _next_ line in the code. | ||
| line_number = line_number.saturating_add(1); | ||
| } |
There was a problem hiding this comment.
Advance stacked own-line assertions to the next actual code line.
This now assumes that the target is always last_assertion_line + 1, but UnparsedAssertionsIter skips blank lines and non-assertion comments. An own-line assertion followed by whitespace or an ordinary comment will be attached to the wrong line, which breaks the documented “next non-assertion line” behavior.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/mdtest/src/assertion.rs` around lines 77 - 115, The current logic
advances stacked own-line assertions by one line (line_number += 1), but
UnparsedAssertionsIter may skip blank lines and non-assertion comments so the
collected own-line comments must attach to the next actual code line; change the
"if only_own_line { line_number = line_number.saturating_add(1); }" behavior to
find and set line_number to the next non-empty, non-comment line (i.e., scan
forward from the current line using file_index.line_index and the source buffer
or by peeking/consuming UnparsedAssertionsIter) so the collector entries created
from CommentRanges::is_own_line(...) attach to the next real code line instead
of blindly +1; keep use of file_assertions.next_if and UnparsedAssertionsIter
semantics when skipping assertions/comments.
| OutputFormat::GitHub => { | ||
| println!( | ||
| "::error file={file},line={line}::{message}", | ||
| message = failure.message() | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
GitHub Actions workflow command escaping rules for ::error annotations: which characters in property values and message text must be percent-encoded or escaped?
💡 Result:
In GitHub Actions workflow commands like ::error, property values (e.g., file={name}, title={title}) and the message text after the final :: must have specific characters percent-encoded to prevent parsing errors. For property values, percent-encode: % (to %25), : (to %3A), , (to %2C), \r (to %0D), \n (to %0A). For message text, percent-encode: % (to %25), \r (to %0D), \n (to %0A). This matches the runner's escaping rules from the actions/runner source code and toolkit issue fixes. Official docs do not explicitly list these but examples avoid problematic characters. Community issues confirm % and : cause failures in similar contexts (e.g., artifacts, cache keys).
Citations:
- 1: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions
- 2: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands
- 3: https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions
- 4: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs
- 5: Fix command escaping to match runner actions/toolkit#301
- 6: Literal colon character (":") in expressions kills YAML parser actions/runner#1019
- 7: String with colon is loaded as object octokit/request-action#118
- 8: [bug] v4
%and#characters in artifact name causes an Authorization error actions/upload-artifact#473 - 9: 404 when key contains slash actions/cache#53
- 10: Files with colons in the name are missing in the artifact. actions/upload-artifact#35
🏁 Script executed:
# First, let's locate the file and check its size
find . -name "lib.rs" -path "*/mdtest/src/*" -type fRepository: RustPython/ruff
Length of output: 86
🏁 Script executed:
# Read the relevant sections from the file
cat -n crates/mdtest/src/lib.rs | sed -n '80,120p'Repository: RustPython/ruff
Length of output: 1735
🏁 Script executed:
# Let's see more context about the failure struct and where the message comes from
cat -n crates/mdtest/src/lib.rs | sed -n '1,86p'Repository: RustPython/ruff
Length of output: 3660
🏁 Script executed:
# Let's look at the Failure struct to understand what message() returns
grep -n "impl Failure" crates/mdtest/src/matcher.rs | head -20Repository: RustPython/ruff
Length of output: 102
🏁 Script executed:
# Read the Failure struct and message() implementation
cat -n crates/mdtest/src/matcher.rs | sed -n '60,120p'Repository: RustPython/ruff
Length of output: 2586
🏁 Script executed:
# Search for where Failure::new is called and what messages are created
rg "Failure::new" crates/mdtest/src/ -B 2 -A 2Repository: RustPython/ruff
Length of output: 2992
🏁 Script executed:
# Let's check what colors are being applied and if they're being included in the message
rg "\.red\(\)|\.yellow\(\)|\.cyan\(\)" crates/mdtest/src/matcher.rsRepository: RustPython/ruff
Length of output: 398
🏁 Script executed:
# Let's check if there's any escaping utility or function used elsewhere for GitHub annotations
rg "escape|%25|%0A|%0D" crates/mdtest/src/Repository: RustPython/ruff
Length of output: 41
🏁 Script executed:
# Let's check if there are any tests or examples that show what kind of content these messages contain
rg "snapshot" crates/mdtest/tests/ -l | head -5Repository: RustPython/ruff
Length of output: 120
🏁 Script executed:
# Let's look at how snapshots are created to understand if they can have problematic characters
rg "snapshot" crates/mdtest/src/lib.rs -B 2 -A 2 | head -40Repository: RustPython/ruff
Length of output: 1560
🏁 Script executed:
# Let's check what render_diff does to understand if it produces multiline output
cat -n crates/mdtest/src/lib.rs | sed -n '140,200p'Repository: RustPython/ruff
Length of output: 2378
🏁 Script executed:
# Find the render_diff function
rg "fn render_diff" crates/mdtest/src/lib.rs -A 30Repository: RustPython/ruff
Length of output: 1077
🏁 Script executed:
# Check if there's any sanitization or escaping of these values before they reach write_error/write_inconsistency
rg "write_error|write_inconsistency" crates/mdtest/src/ -B 5 -A 5 | head -60Repository: RustPython/ruff
Length of output: 1541
🏁 Script executed:
# Find where write_error and write_inconsistency are called
rg "\.write_error|\.write_inconsistency" crates/mdtest/src/ -B 2 -A 2Repository: RustPython/ruff
Length of output: 41
🏁 Script executed:
# Search for calls to write_error and write_inconsistency
rg "write_error\(|write_inconsistency\(" crates/ -B 2 -A 2Repository: RustPython/ruff
Length of output: 1762
Escape special characters in GitHub Actions annotation fields.
The file, line, and fixture_path properties, as well as the message text in both println! calls, must be percent-encoded before interpolation into the workflow command. Per GitHub Actions escaping rules, property values require: % → %25, : → %3A, , → %2C, \r → %0D, \n → %0A. Message text requires: % → %25, \r → %0D, \n → %0A.
This matters because render_diff() produces multiline output with embedded newlines, and file paths can contain colons. Unescaped newlines and colons will cause GitHub Actions to truncate or misparsed the annotation.
Also applies to: 112-113
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/mdtest/src/lib.rs` around lines 87 - 91, In the OutputFormat::GitHub
branch (the println! calls that emit GitHub Actions annotations) percent-encode
the annotation property values and messages before interpolation: apply the
property encoding (%→%25, :→%3A, ,→%2C, \r→%0D, \n→%0A) to file, line (if used
as a string), and fixture_path, and apply the message encoding (%→%25, \r→%0D,
\n→%0A) to failure.message() and render_diff() output; implement helper(s)
(e.g., encode_github_prop and encode_github_msg) and call them where the
println! for OutputFormat::GitHub formats file, line, fixture_path, or message
so annotations are safe for GitHub Actions parsing.
| pub struct FailuresByLine { | ||
| failures: Vec<Failure>, | ||
| lines: Vec<LineFailures>, | ||
| } | ||
|
|
||
| impl FailuresByLine { | ||
| pub(super) fn iter(&self) -> impl Iterator<Item = (OneIndexed, &[String])> { | ||
| pub fn iter(&self) -> impl Iterator<Item = (OneIndexed, &[Failure])> { | ||
| self.lines.iter().map(|line_failures| { |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Make Failure inspectable from outside the crate.
match_file and FailuresByLine::iter() are public, but downstream callers still can't read the returned failures because both fields are private and the getters are pub(super).
🔓 Proposed API fix
impl Failure {
pub fn new(message: impl std::fmt::Display) -> Self {
Self {
message: message.to_string(),
diff: None,
}
}
- pub(super) fn message(&self) -> &str {
+ pub fn message(&self) -> &str {
&self.message
}
- pub(super) fn diff(&self) -> Option<(&str, &str)> {
+ pub fn diff(&self) -> Option<(&str, &str)> {
self.diff
.as_ref()
.map(|(expected, actual)| (expected.as_str(), actual.as_str()))
}Also applies to: 52-82
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/mdtest/src/matcher.rs` around lines 23 - 30, The Failure type and its
internal data are not accessible to downstream callers because Failure (and/or
its fields) are private and existing getters are pub(super); make Failure
inspectable by making the Failure struct public (pub struct Failure) and either
expose its fields as pub or add public accessor methods on Failure (e.g., pub fn
message(&self) -> &str) so callers using match_file and FailuresByLine::iter()
can read failure details; update any related types (LineFailures) visibility as
needed so the iterator returned by FailuresByLine::iter() yields inspectable
Failure values.
| if failures.is_empty() { | ||
| Ok(()) | ||
| // We need to re-sort the diagnostics because matching uses `swap_remove` internally, which can change ordering. | ||
| snapshot_diagnostics | ||
| .sort_unstable_by(|a, b| a.rendering_sort_key(db).cmp(&b.rendering_sort_key(db))); | ||
| Ok(snapshot_diagnostics) |
There was a problem hiding this comment.
Restore snapshot diagnostics with a deterministic tie-breaker.
swap_remove scrambles match order, and sort_unstable_by(rendering_sort_key) still leaves ties nondeterministic. Two diagnostics at the same location can therefore flip in snapshot output even when matching succeeds.
🔁 One localized way to stabilize the order
- snapshot_diagnostics
- .sort_unstable_by(|a, b| a.rendering_sort_key(db).cmp(&b.rendering_sort_key(db)));
+ snapshot_diagnostics.sort_by(|a, b| {
+ a.rendering_sort_key(db)
+ .cmp(&b.rendering_sort_key(db))
+ .then_with(|| a.primary_message().cmp(b.primary_message()))
+ .then_with(|| a.secondary_code_or_id().cmp(b.secondary_code_or_id()))
+ });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/mdtest/src/matcher.rs` around lines 165 - 169, Replace the
non-deterministic sort_unstable_by call in the failures.is_empty() branch with a
deterministic sort that uses rendering_sort_key(db) as the primary key and a
stable secondary tie-breaker (e.g., diagnostic span start/end, the rendered
message text, or a stable diagnostic id) so two diagnostics with equal
rendering_sort_key cannot flip order; update the sort call on
snapshot_diagnostics (the same variable and block where rendering_sort_key(db)
is used) to compare the primary key then cmp the chosen secondary field to
ensure fully deterministic ordering.
| let Some((_, current_file)) = self.current_section_files.last() else { | ||
| let backtick_start = line_number(offsets.start(), self.source); | ||
|
|
||
| bail!( | ||
| "`snapshot` code block on line {backtick_start} must follow a Python code block, but section has no files." | ||
| ); | ||
| }; | ||
|
|
||
| let file = &mut self.files[*current_file]; | ||
|
|
||
| if !file.is_checkable() { | ||
| let backtick_start = line_number(offsets.start(), self.source); | ||
|
|
||
| bail!( | ||
| "`snapshot` code block on line {backtick_start} must follow a `python` code block in the same section but it follows a `{}` block.", | ||
| file.lang | ||
| ); | ||
| } | ||
|
|
||
| let code_block = file.python_code_blocks.last_mut().unwrap(); |
There was a problem hiding this comment.
Require snapshot blocks to bind to the immediately preceding checkable block.
This looks up the last file in the section, not the last parsed markdown block. As a result, a snapshot block can still attach after intervening prose or other non-code content, which silently associates it with the wrong Python block. The parser needs a dedicated “pending snapshot target” that is cleared whenever anything else is parsed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/mdtest/src/parser.rs` around lines 942 - 961, The snapshot-binding
logic currently uses self.current_section_files.last() and
file.python_code_blocks.last_mut().unwrap(), which allows intervening prose to
leave the previous Python block as a target; introduce a dedicated
pending_snapshot_target field on the parser state (e.g., Option<(file_index,
block_index)>) that is set when parsing a checkable Python code block and
cleared whenever any non-code content, new section, or a different block type is
parsed; change the snapshot handler to use pending_snapshot_target (and bail
with a clear error if None) instead of current_section_files.last(), and ensure
code that consumes the python code block updates/clears pending_snapshot_target
as appropriate (update references: current_section_files, files,
python_code_blocks, and the snapshot handling site).
| let code_block = file.python_code_blocks.last_mut().unwrap(); | ||
|
|
||
| if let Some(existing_block) = &code_block.inline_snapshot_block { | ||
| let code_block_start = line_number(code_block.embedded_start_offset(), self.source); | ||
| let backtick_start = line_number(offsets.start(), self.source); | ||
| let existing_start = line_number(existing_block.range.start(), self.source); | ||
|
|
||
| bail!( | ||
| "Python code block on line `{code_block_start}` has more than one `snapshot` block: first on line {existing_start} and another on line {backtick_start}.", |
There was a problem hiding this comment.
Use markdown offsets in the duplicate-snapshot error path.
embedded_start_offset() is relative to the concatenated embedded file, so using it with self.source reports the wrong markdown line number here. The error should use the code block’s markdown fence offset instead.
Suggested fix
- let code_block_start = line_number(code_block.embedded_start_offset(), self.source);
+ let code_block_start = line_number(code_block.backticks.start(), self.source);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/mdtest/src/parser.rs` around lines 961 - 969, The error message
computes the markdown line for the Python code block using
code_block.embedded_start_offset(), which is relative to the concatenated
embedded file and yields the wrong markdown line; update the calculation to use
the code block’s markdown fence offset (the offset that refers into the original
markdown source) when calling line_number, i.e. replace the use of
code_block.embedded_start_offset() with the code-block method/property that
returns the markdown fence/markdown-start offset so code_block_start is computed
against self.source; keep the other offsets (offsets.start() and
existing_block.range.start()) as-is.
| /// The "merge window" for annotations. | ||
| /// | ||
| /// If two annotations have fewer than this number of lines between them, | ||
| /// they will be merged into a single annotation. | ||
| merge_window: usize, |
There was a problem hiding this comment.
Clarify the public merge_window semantics.
These docs say this merges “annotations” based on the literal gap size, but the renderer actually coalesces snippets and clamps the effective window to at least context. As written, callers using merge_window(0) with non-zero context will expect less merging than they actually get.
📝 Proposed doc fix
- /// The "merge window" for annotations.
- ///
- /// If two annotations have fewer than this number of lines between them,
- /// they will be merged into a single annotation.
+ /// Controls how aggressively nearby annotations are coalesced into the same rendered snippet.
+ ///
+ /// The effective merge window is clamped to at least `context`, so overlapping or
+ /// directly adjacent rendered context windows are still merged even when this is `0`.
merge_window: usize,
@@
- /// Set the "merge window" for annotations.
- ///
- /// If two annotations have fewer than this number of lines between them,
- /// they will be merged into a single annotation.
+ /// Set how aggressively nearby annotations are coalesced into the same rendered snippet.
pub fn merge_window(self, lines: usize) -> DisplayDiagnosticConfig {Also applies to: 1437-1446
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/ruff_db/src/diagnostic/mod.rs` around lines 1374 - 1378, Update the
doc comment for the merge_window field to state that merging is applied to
rendered snippets (not raw annotations) and that the effective merge gap is
clamped to at least the configured context (i.e., effective_window =
max(merge_window, context)), so callers using merge_window(0) with non‑zero
context will still merge according to context; apply the same clarification to
the other doc block around the merge semantics (the second comment at 1437-1446)
and reference the fields named merge_window and context in the text.
Summary
Test Plan
Summary by CodeRabbit
Release Notes
New Features
#ruff:ignoreand#ruff:file-ignoredirectives.Bug Fixes
Documentation