Skip to content

Conversation

@danparizher
Copy link
Contributor

Summary

Fixes #20973 (docstring-extraneous-exception) false positive when exceptions mentioned in docstrings are caught and explicitly re-raised using raise e or raise e from None.

Problem Analysis

The DOC502 rule was incorrectly flagging exceptions mentioned in docstrings as "not explicitly raised" when they were actually being explicitly re-raised through exception variables bound in except clauses.

Root Cause: The BodyVisitor in check_docstring.rs only checked for direct exception references (like raise OSError()) but didn't recognize when a variable bound to an exception in an except clause was being re-raised.

Example of the bug:

def f():
    """Do nothing.

    Raises
    ------
    OSError
        If the OS errors.
    """
    try:
        pass
    except OSError as e:
        raise e  # This was incorrectly flagged as not explicitly raising OSError

The issue occurred because resolve_qualified_name(e) couldn't resolve the variable e to a qualified exception name, since e is just a variable binding, not a direct reference to an exception class.

Approach

Modified the BodyVisitor in crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs to:

  1. Track exception variable bindings: Added exception_variables field to map exception variable names to their exception types within except clauses
  2. Enhanced raise statement detection: Updated visit_stmt to check if a raise statement uses a variable name that's bound to an exception in the current except clause
  3. Proper scope management: Clear exception variable mappings when leaving except handlers to prevent cross-contamination

Key changes:

  • Added exception_variables: FxHashMap<&'a str, QualifiedName<'a>> to track variable-to-exception mappings
  • Enhanced visit_except_handler to store exception variable bindings when entering except clauses
  • Modified visit_stmt to check for variable-based re-raising: raise e → lookup e in exception_variables
  • Clear mappings when exiting except handlers to maintain proper scope

@github-actions
Copy link
Contributor

github-actions bot commented Oct 21, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+92 -0 violations, +0 -0 fixes in 2 projects; 53 projects unchanged)

apache/airflow (+91 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

+ airflow-ctl/src/airflowctl/api/client.py:307:5: DOC501 Raised exception `AirflowCtlNotFoundException` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:190:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:204:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:212:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:230:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:241:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:249:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:259:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:267:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:275:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:283:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:291:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:303:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:311:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:319:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:332:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:340:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:348:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:360:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:368:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:380:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:395:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:403:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:411:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:419:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:430:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:443:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:455:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:463:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:529:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:559:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:586:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:598:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:606:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:614:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:622:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:644:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:656:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:664:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:672:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:680:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ airflow-ctl/src/airflowctl/api/operations.py:694:9: DOC501 Raised exception `ServerResponseError` missing from docstring
+ providers/amazon/src/airflow/providers/amazon/aws/hooks/cloud_formation.py:50:9: DOC501 Raised exception `ClientError` missing from docstring
+ providers/amazon/src/airflow/providers/amazon/aws/hooks/dms.py:257:9: DOC501 Raised exception `ClientError` missing from docstring
+ providers/amazon/src/airflow/providers/amazon/aws/hooks/dms.py:321:9: DOC501 Raised exception `ClientError` missing from docstring
+ providers/amazon/src/airflow/providers/amazon/aws/hooks/dynamodb.py:87:9: DOC501 Raised exception `ClientError` missing from docstring
+ providers/amazon/src/airflow/providers/amazon/aws/hooks/logs.py:143:9: DOC501 Raised exception `ClientError` missing from docstring
+ providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py:1537:9: DOC501 Raised exception `ClientError` missing from docstring
+ providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py:1647:9: DOC501 Raised exception `ClientError` missing from docstring
... 42 additional changes omitted for project

bokeh/bokeh (+1 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

+ src/bokeh/server/views/ws.py:182:9: DOC501 Raised exception `ProtocolError` missing from docstring

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
DOC501 92 92 0 0 0

@danparizher
Copy link
Contributor Author

The +109 DOC501 violations represent improved detection accuracy rather than regressions.

From what I'm seeing, the fix correctly identifies cases where exceptions are explicitly re-raised through variables like raise e but weren't being detected before.

Previously, DOC501 missed these patterns because it couldn't resolve the variable e to an exception type, but now it properly recognizes that raise e is indeed raising the exception that e is bound to.

@ntBre ntBre added bug Something isn't working rule Implementing or modifying a lint rule preview Related to preview mode features labels Oct 21, 2025
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thanks! This looks like you're on the right track, but we need to make sure the tests would actually trigger on main, expand them to cover the tricky tuple case, and then I think we can simplify the code a bit too.

This is a minor detail, but would you try to update the PR title too? As the ecosystem report shows (thanks for your analysis there, it looked accurate when I spot-checked), these changes actually have a much larger impact on DOC501 than DOC502. We should mention both rules, though.

Comment on lines 881 to 888
if let ast::Expr::Tuple(tuple) = exceptions {
// For tuple exceptions, we'll store the first one
if let Some(first_exception) = tuple.elts.first() {
store_exception_variable(first_exception);
}
} else {
store_exception_variable(exceptions);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why would we only store the first element in a tuple of exceptions? It seems like we should store all of them to me.

Comment on lines +64 to +82
DOC501 Raised exception `ZeroDivisionError` missing from docstring
--> DOC501_google.py:70:5
|
68 | # DOC501
69 | def calculate_speed(distance: float, time: float) -> float:
70 | / """Calculate speed as distance divided by time.
71 | |
72 | | Args:
73 | | distance: Distance traveled.
74 | | time: Time spent traveling.
75 | |
76 | | Returns:
77 | | Speed as distance divided by time.
78 | | """
| |_______^
79 | try:
80 | return distance / time
|
help: Add `ZeroDivisionError` to the docstring
Copy link
Contributor

Choose a reason for hiding this comment

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

We shouldn't do this in this PR, but this looks like a nice place for a secondary annotation showing where the ZeroDivisionError is raised.

Update the pydoclint rule to correctly handle exception variables bound to tuples in except clauses. Now, when an exception variable is assigned from a tuple, all exception types in the tuple are considered for docstring checks, preventing false positives for DOC502. Test fixtures and snapshots are updated to reflect the improved behavior.
@danparizher danparizher changed the title [pydoclint] Fix false positive on explicit exception re-raising (DOC502) [pydoclint] Fix false positive on explicit exception re-raising (DOC501, DOC502) Oct 24, 2025
@danparizher danparizher changed the title [pydoclint] Fix false positive on explicit exception re-raising (DOC501, DOC502) [pydoclint] Fix false positive on explicit exception re-raising (DOC501, DOC502) Oct 24, 2025
@danparizher danparizher changed the title [pydoclint] Fix false positive on explicit exception re-raising (DOC501, DOC502) [pydoclint] Fix false positive on explicit exception re-raising (DOC501, DOC502) Oct 24, 2025
@danparizher danparizher requested a review from ntBre October 24, 2025 03:39
I also changed the range used in ExceptionEntry from exc.range() to
stmt.range(), like the currently_suspended_exceptions case below.

this range is not used for the diagnostic but for deduplicating/sorting
exceptions referring to the same type. I think either exc.range() or
stmt.range() will accomplish the same goal, but it seemed slightly preferable to
be consistent in that case.

there's still a bit of duplication here with the same Tuple case in both
branches, but that seems okay.
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thanks! This looks good. I pushed one commit factoring out the closure into a method instead of duplicating it. I'll just make sure I didn't break anything in the ecosystem report and then merge this.

@ntBre ntBre merged commit 1ade9a5 into astral-sh:main Oct 24, 2025
37 checks passed
dcreager added a commit that referenced this pull request Oct 27, 2025
* origin/main:
  Respect `--output-format` with `--watch` (#21097)
  [`pyflakes`] Revert to stable behavior if imports for module lie in alternate branches for `F401` (#20878)
  Fix finding keyword range for clause header after statement ending with semicolon (#21067)
  [ty] Fix bug where ty would think all types had an `__mro__` attribute (#20995)
  Restore `indent.py` (#21094)
  [`flake8-django`] Apply `DJ001` to annotated fields (#20907)
  Clearer error message when `line-length` goes beyond threshold (#21072)
  Update upload and download artifacts github actions (#21083)
  Update dependency mdformat-mkdocs to v4.4.2 (#21088)
  Update cargo-bins/cargo-binstall action to v1.15.9 (#21086)
  Update Rust crate clap to v4.5.50 (#21090)
  Update Rust crate get-size2 to v0.7.1 (#21091)
  Update Rust crate bstr to v1.12.1 (#21089)
  Add missing docstring sections to the numpy list (#20931)
  [`pydoclint`] Fix false positive on explicit exception re-raising (`DOC501`, `DOC502`) (#21011)
  [ty] Use constructor parameter types as type context (#21054)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working preview Related to preview mode features rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DOC502 false positive on raise e

2 participants