Skip to content

Add Autofix for ISC003 #18256

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

Merged
merged 15 commits into from
May 28, 2025
Merged

Add Autofix for ISC003 #18256

merged 15 commits into from
May 28, 2025

Conversation

maxmynter
Copy link
Contributor

Closes: #18081

Summary

Add the following fix for ICS003 (explicit string concatenation): Remove the + operator to implicitly concatenate {r,f,b}strings.

Test Plan

Extended and adapted the ICS.py test fixture.

  • Added tests for b and rb strings
  • Updated the snapshot for existing tests to reflect the suggested fix

Copy link
Contributor

github-actions bot commented May 22, 2025

ruff-ecosystem results

Linter (stable)

ℹ️ ecosystem check detected linter changes. (+0 -0 violations, +50 -0 fixes in 3 projects; 52 projects unchanged)

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

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

- superset/db_engine_specs/bigquery.py:81:5: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/bigquery.py:81:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/clickhouse.py:292:17: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/clickhouse.py:292:17: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:37:27: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:37:27: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:39:27: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:39:27: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:41:33: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:41:33: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:43:32: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:43:32: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:45:36: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:45:36: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:47:35: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:47:35: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:52:26: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:52:26: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:54:28: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:54:28: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:68:13: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:68:13: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/viz.py:1395:29: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/viz.py:1395:29: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/viz.py:1435:25: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/viz.py:1435:25: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- tests/integration_tests/db_engine_specs/pinot_tests.py:31:13: ISC003 Explicitly concatenated string should be implicitly concatenated
+ tests/integration_tests/db_engine_specs/pinot_tests.py:31:13: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- tests/integration_tests/db_engine_specs/pinot_tests.py:49:13: ISC003 Explicitly concatenated string should be implicitly concatenated
+ tests/integration_tests/db_engine_specs/pinot_tests.py:49:13: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- tests/integration_tests/db_engine_specs/pinot_tests.py:66:13: ISC003 Explicitly concatenated string should be implicitly concatenated
+ tests/integration_tests/db_engine_specs/pinot_tests.py:66:13: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- tests/integration_tests/db_engine_specs/pinot_tests.py:77:13: ISC003 Explicitly concatenated string should be implicitly concatenated
+ tests/integration_tests/db_engine_specs/pinot_tests.py:77:13: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- tests/integration_tests/utils_tests.py:352:13: ISC003 Explicitly concatenated string should be implicitly concatenated
+ tests/integration_tests/utils_tests.py:352:13: ISC003 [*] Explicitly concatenated string should be implicitly concatenated

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

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

- examples/styling/mathtext/latex_schrodinger.py:26:5: ISC003 Explicitly concatenated string should be implicitly concatenated
+ examples/styling/mathtext/latex_schrodinger.py:26:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- src/bokeh/command/subcommands/serve.py:899:17: ISC003 Explicitly concatenated string should be implicitly concatenated
+ src/bokeh/command/subcommands/serve.py:899:17: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- src/bokeh/server/session.py:90:40: ISC003 Explicitly concatenated string should be implicitly concatenated
+ src/bokeh/server/session.py:90:40: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- src/bokeh/util/compiler.py:411:24: ISC003 Explicitly concatenated string should be implicitly concatenated
+ src/bokeh/util/compiler.py:411:24: ISC003 [*] Explicitly concatenated string should be implicitly concatenated

latchbio/latch (+0 -0 violations, +6 -0 fixes)

- src/latch_cli/centromere/ctx.py:282:29: ISC003 Explicitly concatenated string should be implicitly concatenated
+ src/latch_cli/centromere/ctx.py:282:29: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- src/latch_cli/utils/path.py:181:9: ISC003 Explicitly concatenated string should be implicitly concatenated
+ src/latch_cli/utils/path.py:181:9: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- src/latch_cli/utils/path.py:189:9: ISC003 Explicitly concatenated string should be implicitly concatenated
+ src/latch_cli/utils/path.py:189:9: ISC003 [*] Explicitly concatenated string should be implicitly concatenated

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
ISC003 50 0 0 50 0

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+0 -0 violations, +50 -0 fixes in 3 projects; 52 projects unchanged)

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

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

- superset/db_engine_specs/bigquery.py:81:5: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/bigquery.py:81:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/clickhouse.py:292:17: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/clickhouse.py:292:17: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:37:27: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:37:27: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:39:27: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:39:27: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:41:33: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:41:33: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:43:32: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:43:32: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:45:36: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:45:36: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:47:35: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:47:35: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:52:26: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:52:26: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:54:28: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:54:28: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/db_engine_specs/pinot.py:68:13: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/db_engine_specs/pinot.py:68:13: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/viz.py:1395:29: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/viz.py:1395:29: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- superset/viz.py:1435:25: ISC003 Explicitly concatenated string should be implicitly concatenated
+ superset/viz.py:1435:25: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- tests/integration_tests/db_engine_specs/pinot_tests.py:31:13: ISC003 Explicitly concatenated string should be implicitly concatenated
+ tests/integration_tests/db_engine_specs/pinot_tests.py:31:13: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- tests/integration_tests/db_engine_specs/pinot_tests.py:49:13: ISC003 Explicitly concatenated string should be implicitly concatenated
+ tests/integration_tests/db_engine_specs/pinot_tests.py:49:13: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- tests/integration_tests/db_engine_specs/pinot_tests.py:66:13: ISC003 Explicitly concatenated string should be implicitly concatenated
+ tests/integration_tests/db_engine_specs/pinot_tests.py:66:13: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- tests/integration_tests/db_engine_specs/pinot_tests.py:77:13: ISC003 Explicitly concatenated string should be implicitly concatenated
+ tests/integration_tests/db_engine_specs/pinot_tests.py:77:13: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- tests/integration_tests/utils_tests.py:352:13: ISC003 Explicitly concatenated string should be implicitly concatenated
+ tests/integration_tests/utils_tests.py:352:13: ISC003 [*] Explicitly concatenated string should be implicitly concatenated

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

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

- examples/styling/mathtext/latex_schrodinger.py:26:5: ISC003 Explicitly concatenated string should be implicitly concatenated
+ examples/styling/mathtext/latex_schrodinger.py:26:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- src/bokeh/command/subcommands/serve.py:899:17: ISC003 Explicitly concatenated string should be implicitly concatenated
+ src/bokeh/command/subcommands/serve.py:899:17: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- src/bokeh/server/session.py:90:40: ISC003 Explicitly concatenated string should be implicitly concatenated
+ src/bokeh/server/session.py:90:40: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- src/bokeh/util/compiler.py:411:24: ISC003 Explicitly concatenated string should be implicitly concatenated
+ src/bokeh/util/compiler.py:411:24: ISC003 [*] Explicitly concatenated string should be implicitly concatenated

latchbio/latch (+0 -0 violations, +6 -0 fixes)

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

- src/latch_cli/centromere/ctx.py:282:29: ISC003 Explicitly concatenated string should be implicitly concatenated
+ src/latch_cli/centromere/ctx.py:282:29: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- src/latch_cli/utils/path.py:181:9: ISC003 Explicitly concatenated string should be implicitly concatenated
+ src/latch_cli/utils/path.py:181:9: ISC003 [*] Explicitly concatenated string should be implicitly concatenated
- src/latch_cli/utils/path.py:189:9: ISC003 Explicitly concatenated string should be implicitly concatenated
+ src/latch_cli/utils/path.py:189:9: ISC003 [*] Explicitly concatenated string should be implicitly concatenated

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
ISC003 50 0 0 50 0

@maxmynter
Copy link
Contributor Author

maxmynter commented May 22, 2025

The ecosystem changes all seem to be the change from a violation to a fixable violation.

Do we need a preview gate in this case?

cc: @MichaReiser (because we were talking preview gates on an other PR and you've categorised the issue).

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.

I think we need to change the fix to keep each part on its own line. We should also change the rule (separate PR) to only flag concatenations that can be converted to implicit string concatenations

@MichaReiser MichaReiser added the fixes Related to suggested fixes for violations label May 25, 2025
@maxmynter
Copy link
Contributor Author

Thanks for the review.

Changing ISC003 to only fire when fixable and preserve formatting greatly reduces pattern match boilerplate in the fix.
This also removes the Always / Sometimes fixable inconsistency.

I've implemented the AlwaysFixableViolation and dropped the Option wrapping and try_set_fix handling.

I added mulitline test cases. The Diagnostic only warns for the first violation. But testing with the binary, fixes are applied recursively so the following is fully fixed when using ruff:

cat > test.py << 'EOF'
_ = ("a"
    + "b"
    + "c"
    + "d"
    + "e")
EOF
target/debug/ruff check --diff --select ISC003 --no-cache test.py
--- test.py
+++ test.py
@@ -1,5 +1,5 @@
 _ = ("a"
-    + "b"
-    + "c"
-    + "d"
-    + "e")
+     "b"
+     "c"
+     "d"
+     "e")

Would fix 4 errors.

@maxmynter maxmynter requested a review from MichaReiser May 26, 2025 12:49
@maxmynter
Copy link
Contributor Author

maxmynter commented May 27, 2025

Note: Currently a case like this

_ = (
    "start" +
    variable 
		# leading comment
    + "end"
    + "end"
)

Would not be reformatted. Since the first + is necessary and thus the rule is not recursively applied.

Likewise

_ = (
    "start" +
    "middle" + 
    variable 
    + "end"
    + "end"
)

will only fix the concatenation between "start" and "middle" not between "end" and "end".

Is there another way to fix this other than to make the fix recurse through the whole string rather than rely on the recursive application of the rule?

I feel it would make this implementation a lot more involved. Which I'm happy to try, but wanted to double check. If so, are there other rules you'd recommend to look at as reference?

@maxmynter maxmynter requested a review from MichaReiser May 27, 2025 14:03
@MichaReiser
Copy link
Member

Thank you

@MichaReiser
Copy link
Member

Note: Currently a case like this

_ = (
    "start" +
    variable 
		# leading comment
    + "end"
    + "end"
)

Would not be reformatted. Since the first + is necessary and thus the rule is not recursively applied.

Likewise

_ = (
    "start" +
    "middle" + 
    variable 
    + "end"
    + "end"
)

will only fix the concatenation between "start" and "middle" not between "end" and "end".

Is there another way to fix this other than to make the fix recurse through the whole string rather than rely on the recursive application of the rule?

I feel it would make this implementation a lot more involved. Which I'm happy to try, but wanted to double-check. If so, are there other rules you'd recommend to look at as reference?

Given that this is a pre-existing limitation, I'd prefer to handle this in a separate PR.

I think what we would need to do here is to "flatten-out" the binary expression for operators with the same precedence. We do this in the formatter but it can get a bit involved. But it requires you to recursively visit binary expressions and track what the last left side was (and if the operators have the same precedence and stop traversing if they don't)

@MichaReiser MichaReiser merged commit 6d210dd into astral-sh:main May 28, 2025
34 checks passed
dcreager added a commit that referenced this pull request May 28, 2025
* main:
  [ty] Support ephemeral uv virtual environments (#18335)
  Add a `ViolationMetadata::rule` method (#18234)
  Return `DiagnosticGuard` from `Checker::report_diagnostic` (#18232)
  [flake8_use_pathlib]: Replace os.symlink with Path.symlink_to (PTH211) (#18337)
  [ty] Support cancellation and retry in the server (#18273)
  [ty] Synthetic function-like callables (#18242)
  [ty] Support publishing diagnostics in the server (#18309)
  Add Autofix for ISC003 (#18256)
  [`pyupgrade`]: new rule UP050 (`useless-class-metaclass-type`) (#18334)
  [pycodestyle] Make `E712` suggestion not assume a context (#18328)
carljm added a commit to MatthewMckee4/ruff that referenced this pull request May 28, 2025
* main: (246 commits)
  [ty] Simplify signature types, use them in `CallableType` (astral-sh#18344)
  [ty] Support ephemeral uv virtual environments (astral-sh#18335)
  Add a `ViolationMetadata::rule` method (astral-sh#18234)
  Return `DiagnosticGuard` from `Checker::report_diagnostic` (astral-sh#18232)
  [flake8_use_pathlib]: Replace os.symlink with Path.symlink_to (PTH211) (astral-sh#18337)
  [ty] Support cancellation and retry in the server (astral-sh#18273)
  [ty] Synthetic function-like callables (astral-sh#18242)
  [ty] Support publishing diagnostics in the server (astral-sh#18309)
  Add Autofix for ISC003 (astral-sh#18256)
  [`pyupgrade`]: new rule UP050 (`useless-class-metaclass-type`) (astral-sh#18334)
  [pycodestyle] Make `E712` suggestion not assume a context (astral-sh#18328)
  put similar dunder-call tests next to each other (astral-sh#18343)
  [ty] Derive `PartialOrd, Ord` for `KnownInstanceType` (astral-sh#18340)
  [ty] Simplify `Type::try_bool()` (astral-sh#18342)
  [ty] Simplify `Type::normalized` slightly (astral-sh#18339)
  [ty] Move arviz off the list of selected primer projects (astral-sh#18336)
  [ty] Add --config-file CLI arg (astral-sh#18083)
  [ty] Tell the user why we inferred a certain Python version when reporting version-specific syntax errors (astral-sh#18295)
  [ty] Implement implicit inheritance from `Generic[]` for PEP-695 generic classes (astral-sh#18283)
  [ty] Add hint if async context manager is used in non-async with statement (astral-sh#18299)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
fixes Related to suggested fixes for violations
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Autofix for ISC003
3 participants