Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ruff] Unformatted special comments (RUF037) #14111

Closed
wants to merge 30 commits into from

Conversation

InSyncWithFoo
Copy link
Contributor

Summary

Resolves #10160.

Test Plan

cargo nextest run and cargo insta test.

@InSyncWithFoo
Copy link
Contributor Author

I'm not sure why the snapshot is empty. Debugging showed that new diagnostics are correctly added, yet it's almost as if the rule never ran.

Copy link
Contributor

github-actions bot commented Nov 5, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+1074 -0 violations, +0 -0 fixes in 11 projects; 43 projects unchanged)

RasaHQ/rasa (+2 -0 violations, +0 -0 fixes)

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

+ rasa/core/agent.py:422:95: RUF037 [*] Unformatted special comment
+ rasa/utils/tensorflow/model_data.py:106:110: RUF037 [*] Unformatted special comment

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

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

+ airflow/www/views.py:4936:62: RUF037 [*] Unformatted special comment
+ dev/breeze/src/airflow_breeze/configure_rich_click.py:21:47: RUF037 [*] Unformatted special comment
+ docs/exts/docs_build/errors.py:27:62: RUF037 [*] Unformatted special comment
+ kubernetes_tests/test_kubernetes_executor.py:25:21: RUF037 [*] Unformatted special comment
+ kubernetes_tests/test_other_executors.py:25:21: RUF037 [*] Unformatted special comment
+ providers/src/airflow/providers/edge/models/edge_worker.py:137:36: RUF037 [*] Unformatted special comment
+ providers/src/airflow/providers/edge/models/edge_worker.py:154:41: RUF037 [*] Unformatted special comment
+ providers/src/airflow/providers/google/cloud/hooks/bigquery.py:1154:33: RUF037 [*] Unformatted special comment
+ providers/src/airflow/providers/google/cloud/hooks/compute_ssh.py:39:20: RUF037 [*] Unformatted special comment
+ providers/src/airflow/providers/google/cloud/hooks/mlengine.py:600:42: RUF037 [*] Unformatted special comment
... 80 additional changes omitted for project

apache/superset (+46 -0 violations, +0 -0 fixes)

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

+ superset/commands/chart/export.py:17:3: RUF037 [*] Unformatted special comment
+ superset/commands/dashboard/export.py:17:3: RUF037 [*] Unformatted special comment
+ superset/commands/database/export.py:17:3: RUF037 [*] Unformatted special comment
+ superset/commands/dataset/export.py:17:3: RUF037 [*] Unformatted special comment
+ superset/commands/query/export.py:17:3: RUF037 [*] Unformatted special comment
+ tests/integration_tests/access_tests.py:17:3: RUF037 [*] Unformatted special comment
+ tests/integration_tests/advanced_data_type/api_tests.py:17:3: RUF037 [*] Unformatted special comment
+ tests/integration_tests/annotation_layers/api_tests.py:17:3: RUF037 [*] Unformatted special comment
+ tests/integration_tests/base_api_tests.py:17:3: RUF037 [*] Unformatted special comment
+ tests/integration_tests/base_tests.py:17:3: RUF037 [*] Unformatted special comment
... 36 additional changes omitted for project

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

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

+ release/ui.py:27:24: RUF037 [*] Unformatted special comment
+ src/bokeh/__init__.py:36:18: RUF037 [*] Unformatted special comment
+ src/bokeh/__init__.py:88:28: RUF037 [*] Unformatted special comment
+ src/bokeh/__init__.py:91:31: RUF037 [*] Unformatted special comment
+ src/bokeh/__init__.py:96:19: RUF037 [*] Unformatted special comment
+ src/bokeh/__init__.py:97:72: RUF037 [*] Unformatted special comment
+ src/bokeh/__main__.py:27:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/__init__.py:22:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/application.py:24:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/__init__.py:17:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/code.py:31:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/code_runner.py:17:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/directory.py:43:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/document_lifecycle.py:17:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/function.py:33:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/handler.py:38:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/lifecycle.py:17:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/notebook.py:24:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/request_handler.py:17:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/script.py:41:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/server_lifecycle.py:17:18: RUF037 [*] Unformatted special comment
+ src/bokeh/application/handlers/server_request_handler.py:17:18: RUF037 [*] Unformatted special comment
+ src/bokeh/client/__init__.py:26:18: RUF037 [*] Unformatted special comment
+ src/bokeh/client/connection.py:20:18: RUF037 [*] Unformatted special comment
+ src/bokeh/client/session.py:29:18: RUF037 [*] Unformatted special comment
+ src/bokeh/client/states.py:17:18: RUF037 [*] Unformatted special comment
+ src/bokeh/client/util.py:16:18: RUF037 [*] Unformatted special comment
+ src/bokeh/client/websocket.py:17:18: RUF037 [*] Unformatted special comment
+ src/bokeh/colors/__init__.py:17:18: RUF037 [*] Unformatted special comment
+ src/bokeh/colors/color.py:16:18: RUF037 [*] Unformatted special comment
+ src/bokeh/colors/groups.py:16:18: RUF037 [*] Unformatted special comment
+ src/bokeh/colors/named.py:16:18: RUF037 [*] Unformatted special comment
+ src/bokeh/colors/util.py:16:18: RUF037 [*] Unformatted special comment
+ src/bokeh/command/bootstrap.py:38:18: RUF037 [*] Unformatted special comment
+ src/bokeh/command/subcommand.py:17:18: RUF037 [*] Unformatted special comment
+ src/bokeh/command/subcommands/__init__.py:16:18: RUF037 [*] Unformatted special comment
+ src/bokeh/command/subcommands/build.py:16:18: RUF037 [*] Unformatted special comment
+ src/bokeh/command/subcommands/file_output.py:16:18: RUF037 [*] Unformatted special comment
+ src/bokeh/command/subcommands/info.py:50:18: RUF037 [*] Unformatted special comment
+ src/bokeh/command/subcommands/init.py:16:18: RUF037 [*] Unformatted special comment
... 832 additional changes omitted for project

ibis-project/ibis (+6 -0 violations, +0 -0 fixes)

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

+ .github/workflows/algolia/upload-algolia-api.py:164:56: RUF037 [*] Unformatted special comment
+ .github/workflows/algolia/upload-algolia-api.py:178:58: RUF037 [*] Unformatted special comment
+ .github/workflows/algolia/upload-algolia-api.py:190:68: RUF037 [*] Unformatted special comment
+ .github/workflows/algolia/upload-algolia-api.py:203:55: RUF037 [*] Unformatted special comment
+ .github/workflows/algolia/upload-algolia-api.py:208:68: RUF037 [*] Unformatted special comment
+ ibis/backends/tests/test_client.py:1475:39: RUF037 [*] Unformatted special comment

rotki/rotki (+17 -0 violations, +0 -0 fixes)

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

+ pytestgeventwrapper.py:1:37: RUF037 [*] Unformatted special comment
+ pytestgeventwrapper.py:2:23: RUF037 [*] Unformatted special comment
+ rotkehlchen/__main__.py:1:30: RUF037 [*] Unformatted special comment
+ rotkehlchen/__main__.py:2:23: RUF037 [*] Unformatted special comment
+ rotkehlchen/api/rest.py:2748:70: RUF037 [*] Unformatted special comment
+ rotkehlchen/chain/aggregator.py:312:87: RUF037 [*] Unformatted special comment
+ rotkehlchen/chain/aggregator.py:313:89: RUF037 [*] Unformatted special comment
+ rotkehlchen/chain/aggregator.py:316:85: RUF037 [*] Unformatted special comment
+ rotkehlchen/chain/aggregator.py:319:85: RUF037 [*] Unformatted special comment
+ rotkehlchen/chain/zksync_lite/manager.py:634:52: RUF037 [*] Unformatted special comment
... 7 additional changes omitted for project

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

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

+ zilencer/views.py:809:53: RUF037 [*] Unformatted special comment

indico/indico (+8 -0 violations, +0 -0 fixes)

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

+ indico/core/celery/__init__.py:51:34: RUF037 [*] Unformatted special comment
+ indico/core/db/sqlalchemy/__init__.py:9:26: RUF037 [*] Unformatted special comment
+ indico/core/signals/event/__init__.py:8:3: RUF037 [*] Unformatted special comment
+ indico/modules/categories/serialize.py:55:102: RUF037 [*] Unformatted special comment
+ indico/web/forms/fields/__init__.py:8:3: RUF037 [*] Unformatted special comment
+ indico/web/http_api/__init__.py:8:3: RUF037 [*] Unformatted special comment
+ indico/web/http_api/metadata/serializer.py:53:65: RUF037 [*] Unformatted special comment
+ indico/web/http_api/metadata/serializer.py:54:63: RUF037 [*] Unformatted special comment

python-trio/trio (+0 -0 violations, +0 -0 fixes)

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


wntrblm/nox (+1 -0 violations, +0 -0 fixes)

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

+ nox/_options.py:278:36: RUF037 [*] Unformatted special comment

... Truncated remaining completed project reports due to GitHub comment length restrictions

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
RUF037 1074 1074 0 0 0

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@InSyncWithFoo InSyncWithFoo changed the title [ruff] Unformatted special comments (RUF102) [ruff] Unformatted special comments (RUF104) Nov 6, 2024
@InSyncWithFoo InSyncWithFoo force-pushed the RUF102 branch 3 times, most recently from e54e9a0 to 7e9c370 Compare November 6, 2024 06:10
@InSyncWithFoo InSyncWithFoo marked this pull request as ready for review November 9, 2024 18:14
@InSyncWithFoo InSyncWithFoo marked this pull request as draft November 10, 2024 09:32
@dhruvmanila dhruvmanila added rule Implementing or modifying a lint rule preview Related to preview mode features labels Nov 11, 2024
Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

Thanks for working on this rule.

I think we need to define the scope of this rule more carefully, especially when it comes to non-ruff suppression comments.

  1. All other RUF1XX rules are specific to ruff pragma comments
  2. I don't see us as the authority defining how pyright and mypy comments should be formatted.

I see two possible versions of this rule:

  • One specific to ruff's suppression comments that enforces consistent formatting, including how rule codes are formatted
  • A general purpose rule that handles all kinds of pragma comments. It only enforces a single space between # and the keyword and, optionally, that the colon is followed by a space. We can add a configuration option to support custom pragma codes. This is not a RUF1XX rule.

Comment on lines 54 to 57
// If the file is exempted, ignore all diagnostics,
// save for RUF104, which operates on `# noqa` comments
// and thus needs to be suppressed explicitly.
if !matches!(diagnostic.kind.rule(), Rule::UnformattedSpecialComment)
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain the reasoning for this special handling? I think I would find this behavior surprising and I'm not sure if it justifies the added complexity

Copy link
Contributor Author

@InSyncWithFoo InSyncWithFoo Nov 11, 2024

Choose a reason for hiding this comment

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

blanket-noqa (PGH004) gets similar treatment (right above, line 48), and must either not be enabled or explicitly disabled using per-file-ignores/# ruff: noqa: PGH004 (line 223):

if matches!(diagnostic.kind.rule(), Rule::BlanketNOQA) {
    continue;
}
if settings.rules.enabled(Rule::BlanketNOQA)
    && !per_file_ignores.contains(Rule::BlanketNOQA)
    && !exemption.enumerates(Rule::BlanketNOQA)

RUF104 isn't a noqa-only rule, so unformatted_special_comment() shouldn't be called from check_noqa(). However, exemption is computed within check_noqa() using a function that might emit a warning if a # noqa: comment does not have any codes. This means the necessary information cannot be retrieved from functions other than check_noqa() without undesirable side-effects.

crates/ruff_linter/src/rules/ruff/mod.rs Outdated Show resolved Hide resolved
@MichaReiser
Copy link
Member

@AlexWaygood do you have any recommendations on the rule's scope?

@AlexWaygood
Copy link
Member

I agree that we're getting into very hairy territory if we start formatting other tools' pragma comments by default. However, it does seem useful to have a rule that does generalised formatting of pragma comments.

I like @MichaReiser's second suggestion in #14111 (review) -- I would suggest a rule

  • That is not in the RUF1XX category (which as @MichaReiser says is a category specifically for Ruff pragma comments)
  • That by default, only formats Ruff pragma comments. (And maybe also type: ignore comments, since they are standardised across tools according to a specification?)
  • That can be configured so that it also includes other tool-specific pragma comments such as pyright: ignore, isort: skip_file and pragma: nocover

@InSyncWithFoo
Copy link
Contributor Author

InSyncWithFoo commented Nov 13, 2024

@AlexWaygood

  1. I'll move it to RUF037, then.
  2. Currently the rule is handling (aside from # type) # noqa, # ruff: noqa, # flake8: noqa, # isort, # ruff: isort and # fmt. Aren't these all comments that Ruff reads?
  3. I'm in favor of limiting this rule to just Ruff comments, as I have already rewritten this PR a couple of times and I don't feel like redoing it all over again. I'll give custom comments their own rule in another PR.

@AlexWaygood
Copy link
Member

Currently the rule is handling (aside from # type) # noqa, # ruff: noqa, # flake8: noqa, # isort, # ruff: isort and # fmt. Aren't these all comments that Ruff reads?

They are! I wasn't necessarily criticising anything you were doing -- was just giving my response to Micha's request in #14111 (comment) on what the scope of the rule should be :-)

I'm in favor of limiting this rule to just Ruff comments, as I have already rewritten this PR a couple of times and I don't feel like redoing it all over again. I'll give custom comments their own rule in another PR.

Feel free to keep this PR small and not implement any configurability now. Small, incremental PRs are definitely encouraged! However, if you want to open a followup PR, please make this rule configurable in the followup PR rather than creating a separate rule just for custom comments. I think Micha and I are aligned that a single, configurable rule would be preferable to two rules that do very similar things.

@InSyncWithFoo InSyncWithFoo changed the title [ruff] Unformatted special comments (RUF104) [ruff] Unformatted special comments (RUF037) Nov 13, 2024
@MichaReiser
Copy link
Member

MichaReiser commented Nov 13, 2024

I do think that we can drastically simplify the implementation because we no longer need to special case different comments. All we need is to match for # (fmt|noqa|...) and then see if it has an optional : following it.

Currently the rule is handling (aside from # type) # noqa, # ruff: noqa, # flake8: noqa, # isort, # ruff: isort and # fmt. Aren't these all comments that Ruff reads?

Technically, there's also yapf: disable|enable

@InSyncWithFoo
Copy link
Contributor Author

InSyncWithFoo commented Nov 13, 2024

I do think that we can drastically simplify the implementation because we no longer need to special case different comments.

Special casing # noqa so that all such comments uses , has been my intent all along, and I do want to keep it that way even if it turns out to be a little more complex than necessary. I don't have a strong opinion about the others, though at the moment only valid pragma comments are recognized.

@AlexWaygood If we were to add a "custom" formatter, that would need to be generic enough to apply to most, if not all, pragma comments. The only way to do so (that I can see) is to use regex search-and-replace.

Rust's regex engine guarantees linear time matching, so there is no risk of ReDoS. However, there are also disadvantages: No advanced features. Having no lookarounds, no conditional replacements, no multicaptures and no backreferences means that some comments would require multiple runs to be formatted correctly. For example:

[tool.ruff.lint.format-special-comments]
# `#pyright:ignore[reportFoo,reportBar,reportQux]` -> `# pyright: ignore[reportFoo,reportBar,reportQux]`
# https://regex101.com/r/zd9esV/1
'(?i)#\s*pyright\s*:\s*' = '# pyright: '

# `# pyright: ignore[reportFoo,reportBar,reportQux]` -> `# pyright: ignore[reportFoo, reportBar,reportQux]`
# https://regex101.com/r/zd9esV/2
'(# pyright: ignore\[[^\s,]+(?:, [^\s,]+)*?),(?:\s{2,})?(\S)' = '$1, $2'

# `# pyright: ignore[reportFoo, reportBar,reportQux]` -> `# pyright: ignore[reportFoo, reportBar, reportQux]`
# https://regex101.com/r/zd9esV/3
'(# pyright: ignore\[[^\s,]+(?:, [^\s,]+)*?),(?:\s{2,})?(\S)' = '$1, $2'

# And so on.

With PCRE features, it's much easier (less readable too, though):

# https://regex101.com/r/URCpwl/1
'(?i)(?:(?:(\#\s*pyright\s*:\s*)|\G(?:ignore\[)?[^\s,]+\K,\s*))' = '${1:+# pyright\: :, }'

We could also consider fancy-regex.

@MichaReiser MichaReiser added the needs-decision Awaiting a decision from a maintainer label Nov 13, 2024
@MichaReiser
Copy link
Member

The problem I see with special casing is that it isn't a good fit for a general-purpose, extensible rule; it should instead be a specific rule for noqa.

The question is if we can come up with some general rules that work for all suppression comments without being specific to any pragma comment and that allows easy configuration:

  • Require a space between # and name in # `
  • Require no space left of a : but require a space right of it: # name: ignore ok, # name:ignore not-ok
  • Require no a space left of a , but require a space right of it: # type: ignore[a, b] is ok, type: ignore[a,b] isn't
  • Require no space around [ but require a space right of ] if it isn't the end of the comment.

The rule doesn't require that any suppression comment is valid. E.g. it accepts #type: ignore, a, b, cd t

Note: What's interesting is that e.g. pyright's documentation uses a space before [: # pyright: ignore [reportPrivateUsage, reportGeneralTypeIssues] whereas mypy recommends using no-space # type: ignore[<code>] (which I prefer)

@InSyncWithFoo
Copy link
Contributor Author

I would prefer it if we could have two rules, one for Ruff-specific comments and one customizable, somewhat similar to I001 and I002. I don't want to rewrite #noqa:A123,B456 to just # noqa: A123,B456; # noqa: A123, B456 is clearly better. If special-casing in an extensible rule is so bad, then let's use two rules instead.

@MichaReiser
Copy link
Member

MichaReiser commented Nov 14, 2024

I don't want to rewrite #noqa:A123,B456 to just # noqa: A123,B456; # noqa: A123, B456 is clearly better.

The rule would handle this, when using the rules that I outlined above. What the rule wouldn't handle is that e.g. all rule codes are upper-case (although I'm not sure if ruff even accepts lower case rule codes?)

I'm open to having a noqa specific rule, but I don't think it should handle other ruff suppression comments.

@InSyncWithFoo
Copy link
Contributor Author

although I'm not sure if ruff even accepts lower case rule codes?

# ruff: noqa does, # noqa doesn't, though these codes are not taken into account during suppression. Previously, both of these uses [A-Z]+[0-9]+; Charlie changed both of them recently (#12809), then only reverted the latter (#14229).

@Avasam
Copy link
Contributor

Avasam commented Nov 18, 2024

A general purpose rule that handles all kinds of pragma comments. It only enforces a single space between # and the keyword and, optionally, that the colon is followed by a space. We can add a configuration option to support custom pragma codes.

Making this rule extensible is also what I've suggesting in #10160 (comment) . Even if it comes as a follow-up PR, I'd like if it could handle comments that Ruff doesn't need to be aware of (like pyright)

And if it supports regex somehow, this would superseed TD007 (https://docs.astral.sh/ruff/rules/missing-space-after-todo-colon/) entirely

@MichaReiser
Copy link
Member

I'll close this PR because there's no agreement on the rule's semantics. We should define and align on that first before working on a new PR.

Thank you @InSyncWithFoo for initiating the discussion and creating this pr.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-decision Awaiting a decision from a maintainer 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.

Request: Enforce space after colon (:) for special comments
6 participants