Compare commits

...

24 Commits

Author SHA1 Message Date
Charlie Marsh
90bfc4ec4d Bump version to 0.0.124 2022-11-16 12:25:24 -05:00
Charlie Marsh
b04a6a3f7c Support arbitrary expression paths for class and static decorators (#772) 2022-11-16 12:24:46 -05:00
Charlie Marsh
8ec14e7ee2 Bump version to 0.0.123 2022-11-16 12:06:01 -05:00
Charlie Marsh
17c5cd7c42 Fix off-by-one in noqa map detection (#771)
Fix off-by-one in noqa
2022-11-16 12:00:10 -05:00
Charlie Marsh
7d8360a1de Change all &Option<> to Option<&> (#768) 2022-11-16 09:40:01 -05:00
Harutaka Kawamura
910ee523dd Fix E731 (#766) 2022-11-16 09:17:14 -05:00
Charlie Marsh
72e35a535e Run cargo fmt 2022-11-16 09:15:22 -05:00
Charlie Marsh
b4e1563517 Avoid allocations for binding values (#764) 2022-11-16 08:55:35 -05:00
Charlie Marsh
5717cc97d7 Add references to Flake8 licenses 2022-11-15 23:07:41 -05:00
Charlie Marsh
2c89a19f76 Bump Ruff version to 0.0.122 2022-11-15 22:03:46 -05:00
Charlie Marsh
82fea36bb3 Preserve comments when sorting imports (#749) 2022-11-15 22:02:52 -05:00
Charlie Marsh
63d63e8c12 Increase retry counts in GitHub Actions workflows (#763) 2022-11-15 17:21:16 -05:00
Charlie Marsh
9d136de55a Bump version to 0.0.121 2022-11-15 16:18:39 -05:00
Harutaka Kawamura
1821c07367 Implement B020 (#753) 2022-11-15 16:17:03 -05:00
Charlie Marsh
1fe90ef7f4 Only notify once for each app update (#762) 2022-11-15 16:14:10 -05:00
Charlie Marsh
b5cb9485f6 Move updater to its own module 2022-11-15 15:51:24 -05:00
Charlie Marsh
4d798512b1 Only print version checks on tty (#761) 2022-11-15 15:36:06 -05:00
Charlie Marsh
5f9815b103 Disable auto-updates in JSON mode (#760) 2022-11-15 15:29:10 -05:00
Charlie Marsh
0d3fac1bf9 Add --line-length command line argument (#759) 2022-11-15 12:23:38 -05:00
Charlie Marsh
ff0e5f5cb4 Preserve scopes when checking deferred strings (#758) 2022-11-15 12:19:22 -05:00
Charlie Marsh
374d57d822 Limit PEP 604 checks to Python 3.10+ (#757) 2022-11-15 11:52:12 -05:00
Edgar R. M
85b2a9920f docs: Add flake8-bandit to ToC (#750) 2022-11-15 00:11:39 -05:00
Charlie Marsh
3c22913470 Bump version to 0.0.120 2022-11-14 22:53:36 -05:00
Charlie Marsh
ea03a59b72 De-alias Literal checks (#748) 2022-11-14 22:53:23 -05:00
66 changed files with 1791 additions and 466 deletions

View File

@@ -6,6 +6,11 @@ on:
pull_request:
branches: [main]
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
jobs:
cargo_build:
name: "cargo build"

View File

@@ -10,6 +10,9 @@ env:
PACKAGE_NAME: flake8-to-ruff
CRATE_NAME: flake8_to_ruff
PYTHON_VERSION: "3.7" # to build abi3 wheels
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
jobs:
macos-x86_64:

View File

@@ -12,6 +12,9 @@ concurrency:
env:
PACKAGE_NAME: ruff
PYTHON_VERSION: "3.7" # to build abi3 wheels
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
jobs:
macos-x86_64:

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.119
rev: v0.0.124
hooks:
- id: ruff

7
Cargo.lock generated
View File

@@ -930,7 +930,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.119-dev.0"
version = "0.0.124-dev.0"
dependencies = [
"anyhow",
"clap 4.0.22",
@@ -2238,10 +2238,11 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.119"
version = "0.0.124"
dependencies = [
"anyhow",
"assert_cmd",
"atty",
"bincode",
"bitflags",
"cacache",
@@ -2286,7 +2287,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.119"
version = "0.0.124"
dependencies = [
"anyhow",
"clap 4.0.22",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.119"
version = "0.0.124"
edition = "2021"
[lib]
@@ -14,6 +14,7 @@ name = "ruff"
[dependencies]
anyhow = { version = "1.0.66" }
atty = { version = "0.2.14" }
bincode = { version = "1.3.3" }
bitflags = { version = "1.3.2" }
chrono = { version = "0.4.21", default-features = false, features = ["clock"] }

375
LICENSE
View File

@@ -19,3 +19,378 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The externally maintained libraries from which parts of the Software is derived
are:
- Pyflakes, licensed as follows:
"""
Copyright 2005-2011 Divmod, Inc.
Copyright 2013-2014 Florent Xicluna
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
- autoflake, licensed as follows:
"""
Copyright (C) 2012-2018 Steven Myint
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- Flake8, licensed as follows:
"""
== Flake8 License (MIT) ==
Copyright (C) 2011-2013 Tarek Ziade <tarek@ziade.org>
Copyright (C) 2012-2016 Ian Cordasco <graffatcolmingov@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-2020, licensed as follows:
"""
Copyright (c) 2019 Anthony Sottile
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
- flake8-annotations, licensed as follows:
"""
MIT License
Copyright (c) 2019 - Present S. Co1
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-bandit, licensed as follows:
"""
Copyright (c) 2017 Tyler Wince
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
- flake8-bugbear, licensed as follows:
"""
The MIT License (MIT)
Copyright (c) 2016 Łukasz Langa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-comprehensions, licensed as follows:
"""
MIT License
Copyright (c) 2017 Adam Johnson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-print, licensed as follows:
"""
MIT License
Copyright (c) 2016 Joseph Kahn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-quotes, licensed as follows:
"""
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
- isort, licensed as follows:
"""
The MIT License (MIT)
Copyright (c) 2013 Timothy Edmund Crosley
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
- pep8-naming, licensed as follows:
"""
Copyright © 2013 Florent Xicluna <florent.xicluna@gmail.com>
Licensed under the terms of the Expat License
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- pycodestyle, licensed as follows:
"""
Copyright © 2006-2009 Johann C. Rocholl <johann@rocholl.net>
Copyright © 2009-2014 Florent Xicluna <florent.xicluna@gmail.com>
Copyright © 2014-2020 Ian Lee <IanLee1521@gmail.com>
Licensed under the terms of the Expat License
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- pydocstyle, licensed as follows:
"""
Copyright (c) 2012 GreenSteam, <http://greensteam.dk/>
Copyright (c) 2014-2020 Amir Rachum, <http://amir.rachum.com/>
Copyright (c) 2020 Sambhav Kothari, <https://github.com/samj1912>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- pyupgrade, licensed as follows:
"""
Copyright (c) 2017 Anthony Sottile
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""

View File

@@ -52,15 +52,16 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
4. [pydocstyle](#pydocstyle)
5. [pyupgrade](#pyupgrade)
6. [pep8-naming](#pep8-naming)
7. [flake8-comprehensions](#flake8-comprehensions)
8. [flake8-bugbear](#flake8-bugbear)
9. [flake8-builtins](#flake8-builtins)
10. [flake8-print](#flake8-print)
11. [flake8-quotes](#flake8-quotes)
12. [flake8-annotations](#flake8-annotations)
13. [flake8-2020](#flake8-2020)
14. [Ruff-specific rules](#ruff-specific-rules)
15. [Meta rules](#meta-rules)
7. [flake8-bandit](#flake8-bandit)
8. [flake8-comprehensions](#flake8-comprehensions)
9. [flake8-bugbear](#flake8-bugbear)
10. [flake8-builtins](#flake8-builtins)
11. [flake8-print](#flake8-print)
12. [flake8-quotes](#flake8-quotes)
13. [flake8-annotations](#flake8-annotations)
14. [flake8-2020](#flake8-2020)
15. [Ruff-specific rules](#ruff-specific-rules)
16. [Meta rules](#meta-rules)
5. [Editor Integrations](#editor-integrations)
6. [FAQ](#faq)
7. [Development](#development)
@@ -236,6 +237,8 @@ Options:
Regular expression matching the name of dummy variables
--target-version <TARGET_VERSION>
The minimum Python version that should be supported
--line-length <LINE_LENGTH>
Set the line-length for length-associated checks and automatic formatting
--stdin-filename <STDIN_FILENAME>
The name of the file when passing it through stdin
-h, --help
@@ -528,6 +531,7 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
| B017 | NoAssertRaisesException | `assertRaises(Exception)` should be considered evil | |
| B018 | UselessExpression | Found useless expression. Either assign it to a variable or remove it. | |
| B019 | CachedInstanceMethod | Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks | |
| B020 | LoopVariableOverridesIterator | Loop control variable `...` overrides iterable it iterates | |
| B021 | FStringDocstring | f-string used as docstring. This will be interpreted by python as a joined string rather than a docstring. | |
| B022 | UselessContextlibSuppress | No arguments passed to `contextlib.suppress`. No exceptions will be suppressed and therefore this context manager is redundant | |
| B024 | AbstractBaseClassWithoutAbstractMethod | `...` is an abstract base class, but it has no abstract methods | |
@@ -731,7 +735,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (25/32)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (26/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
Ruff can also replace [`isort`](https://pypi.org/project/isort/), [`yesqa`](https://github.com/asottile/yesqa),

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.119"
version = "0.0.124"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.119"
version = "0.0.124"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.119-dev.0"
version = "0.0.124-dev.0"
edition = "2021"
[lib]

40
resources/test/fixtures/B020.py vendored Normal file
View File

@@ -0,0 +1,40 @@
"""
Should emit:
B020 - on lines 8, 21, and 36
"""
items = [1, 2, 3]
for items in items:
print(items)
items = [1, 2, 3]
for item in items:
print(item)
values = {"secret": 123}
for key, value in values.items():
print(f"{key}, {value}")
for key, values in values.items():
print(f"{key}, {values}")
# Variables defined in a comprehension are local in scope
# to that comprehension and are therefore allowed.
for var in [var for var in range(10)]:
print(var)
for var in (var for var in range(10)):
print(var)
for k, v in {k: v for k, v in zip(range(10), range(10, 20))}.items():
print(k, v)
# However we still call out reassigning the iterable in the comprehension.
for vars in [i for i in vars]:
print(vars)
for var in sorted(range(10), key=lambda var: var.real):
print(var)

View File

@@ -5,7 +5,7 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
"""
_ = """Lorem ipsum dolor sit amet.
_ = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533

View File

@@ -8,14 +8,14 @@ while False:
f = object()
#: E731
f.method = lambda: "Method"
f = {}
#: E731
f["a"] = lambda x: x ** 2
f = []
f.append(lambda x: x ** 2)
f = g = lambda x: x ** 2
lambda: "no-op"

14
resources/test/fixtures/F821_5.py vendored Normal file
View File

@@ -0,0 +1,14 @@
"""Test: inner class annotation."""
class RandomClass:
def random_func(self) -> "InnerClass":
pass
class OuterClass:
class InnerClass:
pass
def failing_func(self) -> "InnerClass":
return self.InnerClass()

View File

@@ -1,5 +1,7 @@
from abc import ABCMeta
import pydantic
class Class:
def bad_method(this):
@@ -21,6 +23,14 @@ class Class:
def static_method(x):
return x
@pydantic.validator
def lower(cls, my_field: str) -> str:
pass
@pydantic.validator("my_field")
def lower(cls, my_field: str) -> str:
pass
def __init__(self):
...

View File

@@ -0,0 +1,7 @@
import os
# This is a comment in the same section, so we need to add one newline.
import sys
import numpy as np
# This is a comment, but it starts a new section, so we don't need to add a newline
# before it.
import leading_prefix

View File

@@ -0,0 +1,25 @@
# Comment 1
# Comment 2
import D
# Comment 3a
import C
# Comment 3b
import C
import B # Comment 4
# Comment 5
# Comment 6
from A import (
a, # Comment 7
b,
c, # Comment 8
)
from A import (
a, # Comment 9
b, # Comment 10
c, # Comment 11
)

View File

@@ -0,0 +1,4 @@
import a
# Don't take this comment into account when determining whether the next import can fit on one line.
from b import c
from d import e # Do take this comment into account when determining whether the next import can fit on one line.

View File

@@ -0,0 +1,11 @@
import io
# Old MacDonald had a farm,
# EIEIO
# And on his farm he had a cow,
# EIEIO
# With a moo-moo here and a moo-moo there
# Here a moo, there a moo, everywhere moo-moo
# Old MacDonald had a farm,
# EIEIO
from errno import EIO
import abc

View File

@@ -0,0 +1,2 @@
import A # type: ignore
from B import C # type: ignore

View File

@@ -33,6 +33,7 @@ ignore-names = [
]
classmethod-decorators = [
"classmethod",
"pydantic.validator",
]
staticmethod-decorators = [
"staticmethod",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.119"
version = "0.0.124"
edition = "2021"
[dependencies]

View File

@@ -225,6 +225,29 @@ pub fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
}
}
/// Format the module name for a relative import.
pub fn format_import_from(level: Option<&usize>, module: Option<&String>) -> String {
let mut module_name = String::with_capacity(16);
if let Some(level) = level {
for _ in 0..*level {
module_name.push('.');
}
}
if let Some(module) = module {
module_name.push_str(module);
}
module_name
}
/// Split a target string (like `typing.List`) into (`typing`, `List`).
pub fn to_module_and_member(target: &str) -> (&str, &str) {
if let Some(index) = target.rfind('.') {
(&target[..index], &target[index + 1..])
} else {
("", target)
}
}
/// Convert a location within a file (relative to `base`) to an absolute
/// position.
pub fn to_absolute(relative: &Location, base: &Location) -> Location {

View File

@@ -54,7 +54,7 @@ pub struct Scope<'a> {
pub id: usize,
pub kind: ScopeKind<'a>,
pub import_starred: bool,
pub values: FnvHashMap<String, Binding>,
pub values: FnvHashMap<&'a str, Binding>,
}
impl<'a> Scope<'a> {
@@ -86,7 +86,7 @@ pub enum BindingKind {
Definition,
Export(Vec<String>),
FutureImportation,
StarImportation,
StarImportation(Option<usize>, Option<String>),
Importation(String, String, BindingContext),
FromImportation(String, String, BindingContext),
SubmoduleImportation(String, String, BindingContext),

View File

@@ -23,7 +23,7 @@ use crate::autofix::fixer;
use crate::message::Message;
use crate::settings::Settings;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Serialize, Deserialize)]
struct CacheMetadata {
@@ -89,7 +89,7 @@ fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> String
format!(
"{}@{}@{}",
path.absolutize().unwrap().to_string_lossy(),
VERSION,
CARGO_PKG_VERSION,
hasher.finish()
)
}

View File

@@ -65,7 +65,7 @@ pub struct Checker<'a> {
scopes: Vec<Scope<'a>>,
scope_stack: Vec<usize>,
dead_scopes: Vec<usize>,
deferred_string_annotations: Vec<(Range, &'a str)>,
deferred_string_annotations: Vec<(Range, &'a str, Vec<usize>, Vec<usize>)>,
deferred_annotations: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
deferred_functions: Vec<(&'a Stmt, Vec<usize>, Vec<usize>, VisibleScope)>,
deferred_lambdas: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
@@ -159,7 +159,13 @@ impl<'a> Checker<'a> {
}
/// Return `true` if the `Expr` is a reference to `typing.${target}`.
pub fn match_typing_module(&self, call_path: &[&str], target: &str) -> bool {
pub fn match_typing_expr(&self, expr: &Expr, target: &str) -> bool {
let call_path = dealias_call_path(collect_call_paths(expr), &self.import_aliases);
self.match_typing_call_path(&call_path, target)
}
/// Return `true` if the call path is a reference to `typing.${target}`.
pub fn match_typing_call_path(&self, call_path: &[&str], target: &str) -> bool {
match_call_path(call_path, "typing", target, &self.from_imports)
|| (typing::in_extensions(target)
&& match_call_path(call_path, "typing_extensions", target, &self.from_imports))
@@ -216,7 +222,7 @@ where
for name in names {
for scope in self.scopes.iter_mut().skip(GLOBAL_SCOPE_INDEX + 1) {
scope.values.insert(
name.to_string(),
name,
Binding {
kind: BindingKind::Assignment,
used: Some((global_scope_id, Range::from_located(stmt))),
@@ -303,6 +309,8 @@ where
name,
decorator_list,
args,
&self.from_imports,
&self.import_aliases,
&self.settings.pep8_naming,
)
{
@@ -316,6 +324,8 @@ where
name,
decorator_list,
args,
&self.from_imports,
&self.import_aliases,
&self.settings.pep8_naming,
) {
self.add_check(check);
@@ -391,7 +401,7 @@ where
self.visit_expr(expr);
}
self.add_binding(
name.to_string(),
name,
Binding {
kind: BindingKind::Definition,
used: None,
@@ -496,7 +506,7 @@ where
let name = alias.node.name.split('.').next().unwrap();
let full_name = &alias.node.name;
self.add_binding(
name.to_string(),
name,
Binding {
kind: BindingKind::SubmoduleImportation(
name.to_string(),
@@ -518,7 +528,7 @@ where
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
let full_name = &alias.node.name;
self.add_binding(
name.to_string(),
name,
Binding {
kind: BindingKind::Importation(
name.to_string(),
@@ -651,7 +661,7 @@ where
if let Some("__future__") = module.as_deref() {
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
self.add_binding(
name.to_string(),
name,
Binding {
kind: BindingKind::FutureImportation,
// Always mark `__future__` imports as used.
@@ -688,16 +698,10 @@ where
));
}
} else if alias.node.name == "*" {
let module_name = format!(
"{}{}",
".".repeat(level.unwrap_or_default()),
module.clone().unwrap_or_else(|| "module".to_string()),
);
self.add_binding(
module_name.to_string(),
"*",
Binding {
kind: BindingKind::StarImportation,
kind: BindingKind::StarImportation(*level, module.clone()),
used: None,
range: Range::from_located(stmt),
},
@@ -708,7 +712,10 @@ where
[*(self.scope_stack.last().expect("No current scope found."))];
if !matches!(scope.kind, ScopeKind::Module) {
self.add_check(Check::new(
CheckKind::ImportStarNotPermitted(module_name.to_string()),
CheckKind::ImportStarNotPermitted(helpers::format_import_from(
level.as_ref(),
module.as_ref(),
)),
Range::from_located(stmt),
));
}
@@ -716,7 +723,10 @@ where
if self.settings.enabled.contains(&CheckCode::F403) {
self.add_check(Check::new(
CheckKind::ImportStarUsed(module_name.to_string()),
CheckKind::ImportStarUsed(helpers::format_import_from(
level.as_ref(),
module.as_ref(),
)),
Range::from_located(stmt),
));
}
@@ -740,7 +750,7 @@ where
Some(parent) => format!("{}.{}", parent, alias.node.name),
};
self.add_binding(
name.to_string(),
name,
Binding {
kind: BindingKind::FromImportation(
name.to_string(),
@@ -856,7 +866,12 @@ where
pyflakes::plugins::assert_tuple(self, stmt, test);
}
if self.settings.enabled.contains(&CheckCode::B011) {
flake8_bugbear::plugins::assert_false(self, stmt, test, msg);
flake8_bugbear::plugins::assert_false(
self,
stmt,
test,
msg.as_ref().map(|expr| expr.deref()),
);
}
if self.settings.enabled.contains(&CheckCode::S101) {
self.add_check(flake8_bandit::plugins::assert_used(stmt));
@@ -867,10 +882,15 @@ where
flake8_bugbear::plugins::assert_raises_exception(self, stmt, items);
}
}
StmtKind::For { target, body, .. } => {
StmtKind::For {
target, body, iter, ..
} => {
if self.settings.enabled.contains(&CheckCode::B007) {
flake8_bugbear::plugins::unused_loop_control_variable(self, target, body);
}
if self.settings.enabled.contains(&CheckCode::B020) {
flake8_bugbear::plugins::loop_variable_overrides_iterator(self, target, iter);
}
}
StmtKind::Try { handlers, .. } => {
if self.settings.enabled.contains(&CheckCode::F707) {
@@ -889,10 +909,12 @@ where
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.enabled.contains(&CheckCode::E731) {
if let Some(check) =
pycodestyle::checks::do_not_assign_lambda(value, Range::from_located(stmt))
{
self.add_check(check);
if let [target] = &targets[..] {
if let Some(check) =
pycodestyle::checks::do_not_assign_lambda(target, value, stmt)
{
self.add_check(check);
}
}
}
if self.settings.enabled.contains(&CheckCode::U001) {
@@ -909,13 +931,12 @@ where
}
}
}
StmtKind::AnnAssign { value, .. } => {
StmtKind::AnnAssign { target, value, .. } => {
if self.settings.enabled.contains(&CheckCode::E731) {
if let Some(value) = value {
if let Some(check) = pycodestyle::checks::do_not_assign_lambda(
value,
Range::from_located(stmt),
) {
if let Some(check) =
pycodestyle::checks::do_not_assign_lambda(target, value, stmt)
{
self.add_check(check);
}
}
@@ -1006,7 +1027,7 @@ where
if let StmtKind::ClassDef { name, .. } = &stmt.node {
self.pop_scope();
self.add_binding(
name.to_string(),
name,
Binding {
kind: BindingKind::ClassDefinition,
used: None,
@@ -1036,8 +1057,12 @@ where
..
} = &expr.node
{
self.deferred_string_annotations
.push((Range::from_located(expr), value));
self.deferred_string_annotations.push((
Range::from_located(expr),
value,
self.scope_stack.clone(),
self.parent_stack.clone(),
));
} else {
self.deferred_annotations.push((
expr,
@@ -1053,12 +1078,12 @@ where
ExprKind::Subscript { value, slice, .. } => {
// Ex) typing.List[...]
if self.settings.enabled.contains(&CheckCode::U007)
&& self.settings.target_version >= PythonVersion::Py39
&& self.settings.target_version >= PythonVersion::Py310
{
pyupgrade::plugins::use_pep604_annotation(self, expr, value, slice);
}
if self.match_typing_module(&collect_call_paths(value), "Literal") {
if self.match_typing_expr(value, "Literal") {
self.in_literal = true;
}
@@ -1115,7 +1140,7 @@ where
self.check_builtin_shadowing(id, Range::from_located(expr), true);
self.handle_node_store(expr, self.current_parent());
self.handle_node_store(id, expr, self.current_parent());
}
ExprContext::Del => self.handle_node_delete(expr),
}
@@ -1563,8 +1588,12 @@ where
..
} => {
if self.in_annotation && !self.in_literal {
self.deferred_string_annotations
.push((Range::from_located(expr), value));
self.deferred_string_annotations.push((
Range::from_located(expr),
value,
self.scope_stack.clone(),
self.parent_stack.clone(),
));
}
if self.settings.enabled.contains(&CheckCode::S104) {
if let Some(check) = flake8_bandit::plugins::hardcoded_bind_all_interfaces(
@@ -1646,12 +1675,12 @@ where
keywords,
} => {
let call_path = dealias_call_path(collect_call_paths(func), &self.import_aliases);
if self.match_typing_module(&call_path, "ForwardRef") {
if self.match_typing_call_path(&call_path, "ForwardRef") {
self.visit_expr(func);
for expr in args {
self.visit_annotation(expr);
}
} else if self.match_typing_module(&call_path, "cast") {
} else if self.match_typing_call_path(&call_path, "cast") {
self.visit_expr(func);
if !args.is_empty() {
self.visit_annotation(&args[0]);
@@ -1659,12 +1688,12 @@ where
for expr in args.iter().skip(1) {
self.visit_expr(expr);
}
} else if self.match_typing_module(&call_path, "NewType") {
} else if self.match_typing_call_path(&call_path, "NewType") {
self.visit_expr(func);
for expr in args.iter().skip(1) {
self.visit_annotation(expr);
}
} else if self.match_typing_module(&call_path, "TypeVar") {
} else if self.match_typing_call_path(&call_path, "TypeVar") {
self.visit_expr(func);
for expr in args.iter().skip(1) {
self.visit_annotation(expr);
@@ -1681,7 +1710,7 @@ where
}
}
}
} else if self.match_typing_module(&call_path, "NamedTuple") {
} else if self.match_typing_call_path(&call_path, "NamedTuple") {
self.visit_expr(func);
// Ex) NamedTuple("a", [("a", int)])
@@ -1713,7 +1742,7 @@ where
let KeywordData { value, .. } = &keyword.node;
self.visit_annotation(value);
}
} else if self.match_typing_module(&call_path, "TypedDict") {
} else if self.match_typing_call_path(&call_path, "TypedDict") {
self.visit_expr(func);
// Ex) TypedDict("a", {"a": int})
@@ -1841,8 +1870,9 @@ where
false,
);
if self.current_scope().values.contains_key(name) {
if self.current_scope().values.contains_key(&name.as_str()) {
self.handle_node_store(
name,
&Expr::new(
excepthandler.location,
excepthandler.end_location.unwrap(),
@@ -1855,8 +1885,9 @@ where
);
}
let definition = self.current_scope().values.get(name).cloned();
let definition = self.current_scope().values.get(&name.as_str()).cloned();
self.handle_node_store(
name,
&Expr::new(
excepthandler.location,
excepthandler.end_location.unwrap(),
@@ -1873,7 +1904,7 @@ where
if let Some(binding) = {
let scope = &mut self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
&scope.values.remove(name)
&scope.values.remove(&name.as_str())
} {
if binding.used.is_none() {
if self.settings.enabled.contains(&CheckCode::F841) {
@@ -1888,7 +1919,7 @@ where
if let Some(binding) = definition {
let scope = &mut self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
scope.values.insert(name.to_string(), binding);
scope.values.insert(name, binding);
}
}
None => walk_excepthandler(self, excepthandler),
@@ -1932,7 +1963,7 @@ where
// Bind, but intentionally avoid walking the annotation, as we handle it
// upstream.
self.add_binding(
arg.node.arg.to_string(),
&arg.node.arg,
Binding {
kind: BindingKind::Argument,
used: None,
@@ -1997,7 +2028,7 @@ fn try_mark_used(scope: &mut Scope, scope_id: usize, id: &str, expr: &Expr) -> b
};
// Mark the sub-importation as used.
if let Some(binding) = scope.values.get_mut(&alias) {
if let Some(binding) = scope.values.get_mut(alias.as_str()) {
binding.used = Some((scope_id, Range::from_located(expr)));
}
true
@@ -2033,7 +2064,7 @@ impl<'a> Checker<'a> {
for builtin in BUILTINS {
scope.values.insert(
(*builtin).to_string(),
builtin,
Binding {
kind: BindingKind::Builtin,
range: Default::default(),
@@ -2043,7 +2074,7 @@ impl<'a> Checker<'a> {
}
for builtin in MAGIC_GLOBALS {
scope.values.insert(
(*builtin).to_string(),
builtin,
Binding {
kind: BindingKind::Builtin,
range: Default::default(),
@@ -2071,23 +2102,26 @@ impl<'a> Checker<'a> {
}
}
fn add_binding(&mut self, name: String, binding: Binding) {
fn add_binding<'b>(&mut self, name: &'b str, binding: Binding)
where
'b: 'a,
{
if self.settings.enabled.contains(&CheckCode::F402) {
let scope = &self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if let Some(existing) = scope.values.get(&name) {
if matches!(binding.kind, BindingKind::LoopVar)
&& matches!(
existing.kind,
BindingKind::Importation(_, _, _)
| BindingKind::FromImportation(_, _, _)
| BindingKind::SubmoduleImportation(_, _, _)
| BindingKind::StarImportation
BindingKind::Importation(..)
| BindingKind::FromImportation(..)
| BindingKind::SubmoduleImportation(..)
| BindingKind::StarImportation(..)
| BindingKind::FutureImportation
)
{
self.add_check(Check::new(
CheckKind::ImportShadowedByLoopVar(
name.clone(),
name.to_string(),
existing.range.location.row(),
),
binding.range,
@@ -2122,6 +2156,7 @@ impl<'a> Checker<'a> {
let mut import_starred = false;
for scope_index in self.scope_stack.iter().rev() {
let scope = &mut self.scopes[*scope_index];
if matches!(scope.kind, ScopeKind::Class(_)) {
if id == "__class__" {
return;
@@ -2144,16 +2179,19 @@ impl<'a> Checker<'a> {
let mut from_list = vec![];
for scope_index in self.scope_stack.iter().rev() {
let scope = &self.scopes[*scope_index];
for (name, binding) in scope.values.iter() {
if matches!(binding.kind, BindingKind::StarImportation) {
from_list.push(name.to_string());
for binding in scope.values.values() {
if let BindingKind::StarImportation(level, module) = &binding.kind {
from_list.push(helpers::format_import_from(
level.as_ref(),
module.as_ref(),
));
}
}
}
from_list.sort();
self.add_check(Check::new(
CheckKind::ImportStarUsage(id.clone(), from_list),
CheckKind::ImportStarUsage(id.to_string(), from_list),
Range::from_located(expr),
));
}
@@ -2184,113 +2222,112 @@ impl<'a> Checker<'a> {
}
}
fn handle_node_store(&mut self, expr: &Expr, parent: &Stmt) {
if let ExprKind::Name { id, .. } = &expr.node {
if self.settings.enabled.contains(&CheckCode::F823) {
let scopes: Vec<&Scope> = self
.scope_stack
.iter()
.map(|index| &self.scopes[*index])
.collect();
if let Some(check) = pyflakes::checks::undefined_local(&scopes, id) {
self.add_check(check);
}
fn handle_node_store<'b>(&mut self, id: &'b str, expr: &Expr, parent: &Stmt)
where
'b: 'a,
{
if self.settings.enabled.contains(&CheckCode::F823) {
let scopes: Vec<&Scope> = self
.scope_stack
.iter()
.map(|index| &self.scopes[*index])
.collect();
if let Some(check) = pyflakes::checks::undefined_local(&scopes, id) {
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::N806) {
if matches!(self.current_scope().kind, ScopeKind::Function(..)) {
pep8_naming::plugins::non_lowercase_variable_in_function(self, expr, parent, id)
}
if self.settings.enabled.contains(&CheckCode::N806) {
if matches!(self.current_scope().kind, ScopeKind::Function(..)) {
pep8_naming::plugins::non_lowercase_variable_in_function(self, expr, parent, id)
}
}
if self.settings.enabled.contains(&CheckCode::N815) {
if matches!(self.current_scope().kind, ScopeKind::Class(..)) {
pep8_naming::plugins::mixed_case_variable_in_class_scope(self, expr, parent, id)
}
if self.settings.enabled.contains(&CheckCode::N815) {
if matches!(self.current_scope().kind, ScopeKind::Class(..)) {
pep8_naming::plugins::mixed_case_variable_in_class_scope(self, expr, parent, id)
}
}
if self.settings.enabled.contains(&CheckCode::N816) {
if matches!(self.current_scope().kind, ScopeKind::Module) {
pep8_naming::plugins::mixed_case_variable_in_global_scope(
self, expr, parent, id,
)
}
}
if matches!(parent.node, StmtKind::AnnAssign { value: None, .. }) {
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Annotation,
used: None,
range: Range::from_located(expr),
},
);
return;
}
// TODO(charlie): Include comprehensions here.
if matches!(
parent.node,
StmtKind::For { .. } | StmtKind::AsyncFor { .. }
) {
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::LoopVar,
used: None,
range: Range::from_located(expr),
},
);
return;
}
if operations::is_unpacking_assignment(parent) {
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Binding,
used: None,
range: Range::from_located(expr),
},
);
return;
}
let current =
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if id == "__all__"
&& matches!(current.kind, ScopeKind::Module)
&& matches!(
parent.node,
StmtKind::Assign { .. }
| StmtKind::AugAssign { .. }
| StmtKind::AnnAssign { .. }
)
{
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Export(extract_all_names(parent, current)),
used: None,
range: Range::from_located(expr),
},
);
return;
if self.settings.enabled.contains(&CheckCode::N816) {
if matches!(self.current_scope().kind, ScopeKind::Module) {
pep8_naming::plugins::mixed_case_variable_in_global_scope(self, expr, parent, id)
}
}
if matches!(parent.node, StmtKind::AnnAssign { value: None, .. }) {
self.add_binding(
id.to_string(),
id,
Binding {
kind: BindingKind::Assignment,
kind: BindingKind::Annotation,
used: None,
range: Range::from_located(expr),
},
);
return;
}
// TODO(charlie): Include comprehensions here.
if matches!(
parent.node,
StmtKind::For { .. } | StmtKind::AsyncFor { .. }
) {
self.add_binding(
id,
Binding {
kind: BindingKind::LoopVar,
used: None,
range: Range::from_located(expr),
},
);
return;
}
if operations::is_unpacking_assignment(parent) {
self.add_binding(
id,
Binding {
kind: BindingKind::Binding,
used: None,
range: Range::from_located(expr),
},
);
return;
}
let current = &self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if id == "__all__"
&& matches!(current.kind, ScopeKind::Module)
&& matches!(
parent.node,
StmtKind::Assign { .. } | StmtKind::AugAssign { .. } | StmtKind::AnnAssign { .. }
)
{
self.add_binding(
id,
Binding {
kind: BindingKind::Export(extract_all_names(parent, current)),
used: None,
range: Range::from_located(expr),
},
);
return;
}
self.add_binding(
id,
Binding {
kind: BindingKind::Assignment,
used: None,
range: Range::from_located(expr),
},
);
}
fn handle_node_delete(&mut self, expr: &Expr) {
fn handle_node_delete<'b>(&mut self, expr: &'b Expr)
where
'b: 'a,
{
if let ExprKind::Name { id, .. } = &expr.node {
if operations::on_conditional_branch(
&mut self
@@ -2304,10 +2341,11 @@ impl<'a> Checker<'a> {
let scope =
&mut self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if scope.values.remove(id).is_none() && self.settings.enabled.contains(&CheckCode::F821)
if scope.values.remove(&id.as_str()).is_none()
&& self.settings.enabled.contains(&CheckCode::F821)
{
self.add_check(Check::new(
CheckKind::UndefinedName(id.clone()),
CheckKind::UndefinedName(id.to_string()),
Range::from_located(expr),
))
}
@@ -2338,8 +2376,8 @@ impl<'a> Checker<'a> {
fn check_deferred_annotations(&mut self) {
while let Some((expr, scopes, parents)) = self.deferred_annotations.pop() {
self.parent_stack = parents;
self.scope_stack = scopes;
self.parent_stack = parents;
self.visit_expr(expr);
}
}
@@ -2348,10 +2386,14 @@ impl<'a> Checker<'a> {
where
'b: 'a,
{
while let Some((range, expression)) = self.deferred_string_annotations.pop() {
let mut stacks = vec![];
while let Some((range, expression, scopes, parents)) =
self.deferred_string_annotations.pop()
{
if let Ok(mut expr) = parser::parse_expression(expression, "<filename>") {
relocate_expr(&mut expr, range);
allocator.push(expr);
stacks.push((scopes, parents));
} else {
if self.settings.enabled.contains(&CheckCode::F722) {
self.add_check(Check::new(
@@ -2361,7 +2403,9 @@ impl<'a> Checker<'a> {
}
}
}
for expr in allocator {
for (expr, (scopes, parents)) in allocator.iter().zip(stacks) {
self.scope_stack = scopes;
self.parent_stack = parents;
self.visit_expr(expr);
}
}
@@ -2433,16 +2477,19 @@ impl<'a> Checker<'a> {
let mut checks: Vec<Check> = vec![];
for scope in self.dead_scopes.iter().map(|index| &self.scopes[*index]) {
let all_binding = scope.values.get("__all__");
let all_names = all_binding.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => Some(names),
_ => None,
});
let all_binding: Option<&Binding> = scope.values.get("__all__");
let all_names: Option<Vec<&str>> =
all_binding.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => {
Some(names.iter().map(|name| name.as_str()).collect())
}
_ => None,
});
if self.settings.enabled.contains(&CheckCode::F822) {
if !scope.import_starred && !self.path.ends_with("__init__.py") {
if let Some(all_binding) = all_binding {
if let Some(names) = all_names {
if let Some(names) = &all_names {
for name in names {
if !scope.values.contains_key(name) {
checks.push(Check::new(
@@ -2459,11 +2506,14 @@ impl<'a> Checker<'a> {
if self.settings.enabled.contains(&CheckCode::F405) {
if scope.import_starred {
if let Some(all_binding) = all_binding {
if let Some(names) = all_names {
if let Some(names) = &all_names {
let mut from_list = vec![];
for (name, binding) in scope.values.iter() {
if matches!(binding.kind, BindingKind::StarImportation) {
from_list.push(name.to_string());
for binding in scope.values.values() {
if let BindingKind::StarImportation(level, module) = &binding.kind {
from_list.push(helpers::format_import_from(
level.as_ref(),
module.as_ref(),
));
}
}
from_list.sort();
@@ -2471,7 +2521,10 @@ impl<'a> Checker<'a> {
for name in names {
if !scope.values.contains_key(name) {
checks.push(Check::new(
CheckKind::ImportStarUsage(name.clone(), from_list.clone()),
CheckKind::ImportStarUsage(
name.to_string(),
from_list.clone(),
),
all_binding.range,
));
}
@@ -2488,8 +2541,18 @@ impl<'a> Checker<'a> {
BTreeMap::new();
for (name, binding) in scope.values.iter() {
if !matches!(
binding.kind,
BindingKind::Importation(..)
| BindingKind::SubmoduleImportation(..)
| BindingKind::FromImportation(..)
) {
continue;
}
let used = binding.used.is_some()
|| all_names
.as_ref()
.map(|names| names.contains(name))
.unwrap_or_default();
@@ -2516,7 +2579,7 @@ impl<'a> Checker<'a> {
.or_default()
.push(full_name);
}
_ => {}
_ => unreachable!("Already filtered on BindingKind."),
}
}
}

View File

@@ -58,10 +58,7 @@ pub fn check_lines(
// Grab the noqa (logical) line number for the current (physical) line.
// If there are newlines at the end of the file, they won't be represented in
// `noqa_line_for`, so fallback to the current line.
let noqa_lineno = noqa_line_for
.get(&lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
// Enforce unnecessary coding comments (U009).
if enforce_unnecessary_coding_comment {
@@ -155,10 +152,7 @@ pub fn check_lines(
// want to raise W292 anyway).
if let Some(line) = lines.last() {
let lineno = lines.len() - 1;
let noqa_lineno = noqa_line_for
.get(&lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
let noqa = noqa_directives
.entry(noqa_lineno)

View File

@@ -95,6 +95,7 @@ pub enum CheckCode {
B017,
B018,
B019,
B020,
B021,
B022,
B024,
@@ -399,6 +400,7 @@ pub enum CheckKind {
NoAssertRaisesException,
UselessExpression,
CachedInstanceMethod,
LoopVariableOverridesIterator(String),
FStringDocstring,
UselessContextlibSuppress,
AbstractBaseClassWithoutAbstractMethod(String),
@@ -645,6 +647,7 @@ impl CheckCode {
CheckCode::B017 => CheckKind::NoAssertRaisesException,
CheckCode::B018 => CheckKind::UselessExpression,
CheckCode::B019 => CheckKind::CachedInstanceMethod,
CheckCode::B020 => CheckKind::LoopVariableOverridesIterator("...".to_string()),
CheckCode::B021 => CheckKind::FStringDocstring,
CheckCode::B022 => CheckKind::UselessContextlibSuppress,
CheckCode::B024 => CheckKind::AbstractBaseClassWithoutAbstractMethod("...".to_string()),
@@ -890,6 +893,7 @@ impl CheckCode {
CheckCode::B017 => CheckCategory::Flake8Bugbear,
CheckCode::B018 => CheckCategory::Flake8Bugbear,
CheckCode::B019 => CheckCategory::Flake8Bugbear,
CheckCode::B020 => CheckCategory::Flake8Bugbear,
CheckCode::B021 => CheckCategory::Flake8Bugbear,
CheckCode::B022 => CheckCategory::Flake8Bugbear,
CheckCode::B024 => CheckCategory::Flake8Bugbear,
@@ -1098,6 +1102,7 @@ impl CheckKind {
CheckKind::NoAssertRaisesException => &CheckCode::B017,
CheckKind::UselessExpression => &CheckCode::B018,
CheckKind::CachedInstanceMethod => &CheckCode::B019,
CheckKind::LoopVariableOverridesIterator(_) => &CheckCode::B020,
CheckKind::FStringDocstring => &CheckCode::B021,
CheckKind::UselessContextlibSuppress => &CheckCode::B022,
CheckKind::AbstractBaseClassWithoutAbstractMethod(_) => &CheckCode::B024,
@@ -1472,6 +1477,9 @@ impl CheckKind {
CheckKind::CachedInstanceMethod => "Use of `functools.lru_cache` or `functools.cache` \
on methods can lead to memory leaks"
.to_string(),
CheckKind::LoopVariableOverridesIterator(name) => {
format!("Loop control variable `{name}` overrides iterable it iterates")
}
CheckKind::FStringDocstring => "f-string used as docstring. This will be interpreted \
by python as a joined string rather than a docstring."
.to_string(),

View File

@@ -56,6 +56,7 @@ pub enum CheckCodePrefix {
B018,
B019,
B02,
B020,
B021,
B022,
B024,
@@ -386,6 +387,7 @@ impl CheckCodePrefix {
CheckCode::B017,
CheckCode::B018,
CheckCode::B019,
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B024,
@@ -412,6 +414,7 @@ impl CheckCodePrefix {
CheckCode::B017,
CheckCode::B018,
CheckCode::B019,
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B024,
@@ -460,6 +463,7 @@ impl CheckCodePrefix {
CheckCodePrefix::B018 => vec![CheckCode::B018],
CheckCodePrefix::B019 => vec![CheckCode::B019],
CheckCodePrefix::B02 => vec![
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B024,
@@ -467,6 +471,7 @@ impl CheckCodePrefix {
CheckCode::B026,
CheckCode::B027,
],
CheckCodePrefix::B020 => vec![CheckCode::B020],
CheckCodePrefix::B021 => vec![CheckCode::B021],
CheckCodePrefix::B022 => vec![CheckCode::B022],
CheckCodePrefix::B024 => vec![CheckCode::B024],
@@ -1213,6 +1218,7 @@ impl CheckCodePrefix {
CheckCodePrefix::B018 => PrefixSpecificity::Explicit,
CheckCodePrefix::B019 => PrefixSpecificity::Explicit,
CheckCodePrefix::B02 => PrefixSpecificity::Tens,
CheckCodePrefix::B020 => PrefixSpecificity::Explicit,
CheckCodePrefix::B021 => PrefixSpecificity::Explicit,
CheckCodePrefix::B022 => PrefixSpecificity::Explicit,
CheckCodePrefix::B024 => PrefixSpecificity::Explicit,

View File

@@ -87,6 +87,10 @@ pub struct Cli {
/// The minimum Python version that should be supported.
#[arg(long)]
pub target_version: Option<PythonVersion>,
/// Set the line-length for length-associated checks and automatic
/// formatting.
#[arg(long)]
pub line_length: Option<usize>,
/// Round-trip auto-formatting.
// TODO(charlie): This should be a sub-command.
#[arg(long, hide = true)]
@@ -120,6 +124,8 @@ pub fn extract_log_level(cli: &Cli) -> LogLevel {
LogLevel::Quiet
} else if cli.verbose {
LogLevel::Verbose
} else if matches!(cli.format, SerializationFormat::Json) {
LogLevel::Quiet
} else {
LogLevel::Default
}
@@ -146,7 +152,7 @@ pub fn warn_on(
cli_ignore: &[CheckCodePrefix],
cli_extend_ignore: &[CheckCodePrefix],
pyproject_configuration: &Configuration,
pyproject_path: &Option<PathBuf>,
pyproject_path: Option<&PathBuf>,
) {
for code in codes {
if !cli_ignore.is_empty() {
@@ -186,7 +192,7 @@ pub fn warn_on(
/// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`.
pub fn collect_per_file_ignores(
pairs: Vec<PatternPrefixPair>,
project_root: &Option<PathBuf>,
project_root: Option<&PathBuf>,
) -> Vec<PerFileIgnore> {
let mut per_file_ignores: FnvHashMap<String, Vec<CheckCodePrefix>> = FnvHashMap::default();
for pair in pairs {

View File

@@ -1,6 +1,7 @@
use std::ops::Deref;
use rustpython_ast::{Arguments, Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::ast::helpers::collect_call_paths;
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
@@ -12,7 +13,7 @@ use crate::{visibility, Check};
#[derive(Default)]
struct ReturnStatementVisitor<'a> {
returns: Vec<&'a Option<Box<Expr>>>,
returns: Vec<Option<&'a Expr>>,
}
impl<'a, 'b> Visitor<'b> for ReturnStatementVisitor<'a>
@@ -24,7 +25,9 @@ where
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
// No recurse.
}
StmtKind::Return { value } => self.returns.push(value),
StmtKind::Return { value } => {
self.returns.push(value.as_ref().map(|expr| expr.deref()))
}
_ => visitor::walk_stmt(self, stmt),
}
}
@@ -54,7 +57,7 @@ fn check_dynamically_typed<F>(checker: &mut Checker, annotation: &Expr, func: F)
where
F: FnOnce() -> String,
{
if checker.match_typing_module(&collect_call_paths(annotation), "Any") {
if checker.match_typing_expr(annotation, "Any") {
checker.add_check(Check::new(
CheckKind::DynamicallyTypedExpression(func()),
Range::from_located(annotation),

View File

@@ -6,7 +6,7 @@ use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::code_gen::SourceGenerator;
fn assertion_error(msg: &Option<Box<Expr>>) -> Stmt {
fn assertion_error(msg: Option<&Expr>) -> Stmt {
Stmt::new(
Default::default(),
Default::default(),
@@ -24,7 +24,7 @@ fn assertion_error(msg: &Option<Box<Expr>>) -> Stmt {
},
)),
args: if let Some(msg) = msg {
vec![*msg.clone()]
vec![msg.clone()]
} else {
vec![]
},
@@ -36,7 +36,7 @@ fn assertion_error(msg: &Option<Box<Expr>>) -> Stmt {
)
}
pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: &Option<Box<Expr>>) {
pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: Option<&Expr>) {
if let ExprKind::Constant {
value: Constant::Bool(false),
..

View File

@@ -2,7 +2,7 @@ use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Arguments, Constant, Expr, ExprKind};
use crate::ast::helpers::{
collect_call_paths, compose_call_path, dealias_call_path, match_call_path,
collect_call_paths, compose_call_path, dealias_call_path, match_call_path, to_module_and_member,
};
use crate::ast::types::Range;
use crate::ast::visitor;
@@ -104,14 +104,7 @@ pub fn function_call_argument_default(checker: &mut Checker, arguments: &Argumen
.flake8_bugbear
.extend_immutable_calls
.iter()
.map(|s| {
let s = s.as_str();
if let Some(index) = s.rfind('.') {
(&s[..index], &s[index + 1..])
} else {
("", s)
}
})
.map(|target| to_module_and_member(target))
.collect();
let mut visitor = ArgumentDefaultVisitor {
checks: vec![],

View File

@@ -0,0 +1,64 @@
use fnv::FnvHashMap;
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
#[derive(Default)]
struct NameFinder<'a> {
names: FnvHashMap<&'a str, &'a Expr>,
}
impl<'a, 'b> Visitor<'b> for NameFinder<'a>
where
'b: 'a,
{
fn visit_expr(&mut self, expr: &'b Expr) {
match &expr.node {
ExprKind::Name { id, .. } => {
self.names.insert(id, expr);
}
ExprKind::ListComp { generators, .. }
| ExprKind::DictComp { generators, .. }
| ExprKind::SetComp { generators, .. }
| ExprKind::GeneratorExp { generators, .. } => {
for comp in generators {
self.visit_expr(&comp.iter);
}
}
ExprKind::Lambda { args, body } => {
visitor::walk_expr(self, body);
for arg in args.args.iter() {
self.names.remove(arg.node.arg.as_str());
}
}
_ => visitor::walk_expr(self, expr),
}
}
}
/// B020
pub fn loop_variable_overrides_iterator(checker: &mut Checker, target: &Expr, iter: &Expr) {
let target_names = {
let mut target_finder = NameFinder::default();
target_finder.visit_expr(target);
target_finder.names
};
let iter_names = {
let mut iter_finder = NameFinder::default();
iter_finder.visit_expr(iter);
iter_finder.names
};
for (name, expr) in target_names {
if iter_names.contains_key(name) {
checker.add_check(Check::new(
CheckKind::LoopVariableOverridesIterator(name.to_string()),
Range::from_located(expr),
));
}
}
}

View File

@@ -9,6 +9,7 @@ pub use f_string_docstring::f_string_docstring;
pub use function_call_argument_default::function_call_argument_default;
pub use getattr_with_constant::getattr_with_constant;
pub use jump_statement_in_finally::jump_statement_in_finally;
pub use loop_variable_overrides_iterator::loop_variable_overrides_iterator;
pub use mutable_argument_default::mutable_argument_default;
pub use redundant_tuple_in_exception_handler::redundant_tuple_in_exception_handler;
pub use setattr_with_constant::setattr_with_constant;
@@ -32,6 +33,7 @@ mod f_string_docstring;
mod function_call_argument_default;
mod getattr_with_constant;
mod jump_statement_in_finally;
mod loop_variable_overrides_iterator;
mod mutable_argument_default;
mod redundant_tuple_in_exception_handler;
mod setattr_with_constant;

View File

@@ -202,7 +202,7 @@ mod tests {
let path = Path::new("foo").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"foo",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
@@ -210,7 +210,7 @@ mod tests {
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"bar",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
@@ -220,7 +220,7 @@ mod tests {
.unwrap();
let exclude = vec![FilePattern::from_user(
"baz.py",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
@@ -228,7 +228,7 @@ mod tests {
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
@@ -238,7 +238,7 @@ mod tests {
.unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar/baz.py",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
@@ -248,7 +248,7 @@ mod tests {
.unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar/*.py",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
@@ -258,7 +258,7 @@ mod tests {
.unwrap();
let exclude = vec![FilePattern::from_user(
"baz",
&Some(project_root.to_path_buf()),
Some(&project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(!is_excluded(file_path, file_basename, exclude.iter()));

View File

@@ -15,13 +15,13 @@ pub enum ImportType {
pub fn categorize(
module_base: &str,
level: &Option<usize>,
level: Option<&usize>,
src: &[PathBuf],
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
extra_standard_library: &BTreeSet<String>,
) -> ImportType {
if level.map(|level| level > 0).unwrap_or(false) {
if level.map(|level| *level > 0).unwrap_or(false) {
ImportType::LocalFolder
} else if known_first_party.contains(module_base) {
ImportType::FirstParty

40
src/isort/comments.rs Normal file
View File

@@ -0,0 +1,40 @@
use std::borrow::Cow;
use rustpython_ast::Location;
use rustpython_parser::lexer;
use rustpython_parser::lexer::Tok;
use crate::ast::helpers;
use crate::ast::types::Range;
use crate::SourceCodeLocator;
#[derive(Debug)]
pub struct Comment<'a> {
pub value: Cow<'a, str>,
pub location: Location,
pub end_location: Location,
}
/// Collect all comments in an import block.
pub fn collect_comments<'a>(range: &Range, locator: &'a SourceCodeLocator) -> Vec<Comment<'a>> {
let contents = locator.slice_source_code_range(range);
lexer::make_tokenizer(&contents)
.flatten()
.filter_map(|(start, tok, end)| {
if matches!(tok, Tok::Comment) {
let start = helpers::to_absolute(&start, &range.location);
let end = helpers::to_absolute(&end, &range.location);
Some(Comment {
value: locator.slice_source_code_range(&Range {
location: start,
end_location: end,
}),
location: start,
end_location: end,
})
} else {
None
}
})
.collect()
}

182
src/isort/format.rs Normal file
View File

@@ -0,0 +1,182 @@
use crate::isort::types::{AliasData, CommentSet, ImportFromData, Importable};
// Hard-code four-space indentation for the imports themselves, to match Black.
const INDENT: &str = " ";
// Guess a capacity to use for string allocation.
const CAPACITY: usize = 200;
/// Add a plain import statement to the `RopeBuilder`.
pub fn format_import(alias: &AliasData, comments: &CommentSet, is_first: bool) -> String {
let mut output = String::with_capacity(CAPACITY);
if !is_first && !comments.atop.is_empty() {
output.push('\n');
}
for comment in &comments.atop {
output.push_str(comment);
output.push('\n');
}
if let Some(asname) = alias.asname {
output.push_str("import ");
output.push_str(alias.name);
output.push_str(" as ");
output.push_str(asname);
} else {
output.push_str("import ");
output.push_str(alias.name);
}
for comment in &comments.inline {
output.push_str(" ");
output.push_str(comment);
}
output.push('\n');
output
}
/// Add an import-from statement to the `RopeBuilder`.
pub fn format_import_from(
import_from: &ImportFromData,
comments: &CommentSet,
aliases: &[(AliasData, CommentSet)],
line_length: &usize,
is_first: bool,
) -> String {
// We can only inline if: (1) none of the aliases have atop comments, and (3)
// only the last alias (if any) has inline comments.
if aliases
.iter()
.all(|(_, CommentSet { atop, .. })| atop.is_empty())
&& aliases
.iter()
.rev()
.skip(1)
.all(|(_, CommentSet { inline, .. })| inline.is_empty())
{
let (single_line, import_length) =
format_single_line(import_from, comments, aliases, is_first);
if import_length <= *line_length {
return single_line;
}
}
format_multi_line(import_from, comments, aliases, is_first)
}
/// Format an import-from statement in single-line format.
///
/// This method assumes that the output source code is syntactically valid.
fn format_single_line(
import_from: &ImportFromData,
comments: &CommentSet,
aliases: &[(AliasData, CommentSet)],
is_first: bool,
) -> (String, usize) {
let mut output = String::with_capacity(CAPACITY);
let mut line_length = 0;
if !is_first && !comments.atop.is_empty() {
output.push('\n');
}
for comment in &comments.atop {
output.push_str(comment);
output.push('\n');
}
let module_name = import_from.module_name();
output.push_str("from ");
output.push_str(&module_name);
output.push_str(" import ");
line_length += 5 + module_name.len() + 8;
for (index, (AliasData { name, asname }, comments)) in aliases.iter().enumerate() {
if let Some(asname) = asname {
output.push_str(name);
output.push_str(" as ");
output.push_str(asname);
line_length += name.len() + 4 + asname.len();
} else {
output.push_str(name);
line_length += name.len();
}
if index < aliases.len() - 1 {
output.push_str(", ");
line_length += 2;
}
for comment in &comments.inline {
output.push(' ');
output.push(' ');
output.push_str(comment);
line_length += 2 + comment.len();
}
}
for comment in &comments.inline {
output.push(' ');
output.push(' ');
output.push_str(comment);
line_length += 2 + comment.len();
}
output.push('\n');
(output, line_length)
}
/// Format an import-from statement in multi-line format.
fn format_multi_line(
import_from: &ImportFromData,
comments: &CommentSet,
aliases: &[(AliasData, CommentSet)],
is_first: bool,
) -> String {
let mut output = String::with_capacity(CAPACITY);
if !is_first && !comments.atop.is_empty() {
output.push('\n');
}
for comment in &comments.atop {
output.push_str(comment);
output.push('\n');
}
output.push_str("from ");
output.push_str(&import_from.module_name());
output.push_str(" import ");
output.push('(');
for comment in &comments.inline {
output.push(' ');
output.push(' ');
output.push_str(comment);
}
output.push('\n');
for (AliasData { name, asname }, comments) in aliases {
for comment in &comments.atop {
output.push_str(INDENT);
output.push_str(comment);
output.push('\n');
}
output.push_str(INDENT);
if let Some(asname) = asname {
output.push_str(name);
output.push_str(" as ");
output.push_str(asname);
} else {
output.push_str(name);
}
output.push(',');
for comment in &comments.inline {
output.push(' ');
output.push(' ');
output.push_str(comment);
}
output.push('\n');
}
output.push(')');
output.push('\n');
output
}

View File

@@ -1,64 +1,266 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use fnv::FnvHashSet;
use fnv::FnvHashMap;
use itertools::Itertools;
use ropey::RopeBuilder;
use rustpython_ast::{Stmt, StmtKind};
use crate::isort::categorize::{categorize, ImportType};
use crate::isort::comments::Comment;
use crate::isort::sorting::{member_key, module_key};
use crate::isort::types::{AliasData, ImportBlock, ImportFromData, Importable, OrderedImportBlock};
use crate::isort::types::{
AliasData, CommentSet, ImportBlock, ImportFromData, Importable, OrderedImportBlock,
};
mod categorize;
mod comments;
pub mod format;
pub mod plugins;
pub mod settings;
mod sorting;
pub mod track;
mod types;
// Hard-code four-space indentation for the imports themselves, to match Black.
const INDENT: &str = " ";
#[derive(Debug)]
pub struct AnnotatedAliasData<'a> {
pub name: &'a str,
pub asname: Option<&'a String>,
pub atop: Vec<Comment<'a>>,
pub inline: Vec<Comment<'a>>,
}
#[derive(Debug)]
pub enum AnnotatedImport<'a> {
Import {
names: Vec<AliasData<'a>>,
atop: Vec<Comment<'a>>,
inline: Vec<Comment<'a>>,
},
ImportFrom {
module: Option<&'a String>,
names: Vec<AnnotatedAliasData<'a>>,
level: Option<&'a usize>,
atop: Vec<Comment<'a>>,
inline: Vec<Comment<'a>>,
},
}
fn normalize_imports<'a>(imports: &'a [&'a Stmt]) -> ImportBlock<'a> {
let mut block: ImportBlock = Default::default();
fn annotate_imports<'a>(
imports: &'a [&'a Stmt],
comments: Vec<Comment<'a>>,
) -> Vec<AnnotatedImport<'a>> {
let mut annotated = vec![];
let mut comments_iter = comments.into_iter().peekable();
for import in imports {
match &import.node {
StmtKind::Import { names } => {
for name in names {
block.import.insert(AliasData {
name: &name.node.name,
asname: &name.node.asname,
});
// Find comments above.
let mut atop = vec![];
while let Some(comment) =
comments_iter.next_if(|comment| comment.location.row() < import.location.row())
{
atop.push(comment);
}
// Find comments inline.
let mut inline = vec![];
while let Some(comment) = comments_iter.next_if(|comment| {
comment.end_location.row() == import.end_location.unwrap().row()
}) {
inline.push(comment);
}
annotated.push(AnnotatedImport::Import {
names: names
.iter()
.map(|alias| AliasData {
name: &alias.node.name,
asname: alias.node.asname.as_ref(),
})
.collect(),
atop,
inline,
});
}
StmtKind::ImportFrom {
module,
names,
level,
} => {
for name in names {
if name.node.asname.is_none() {
block
// Find comments above.
let mut atop = vec![];
while let Some(comment) =
comments_iter.next_if(|comment| comment.location.row() < import.location.row())
{
atop.push(comment);
}
// Find comments inline.
let mut inline = vec![];
while let Some(comment) =
comments_iter.next_if(|comment| comment.location.row() == import.location.row())
{
inline.push(comment);
}
// Capture names.
let mut aliases = vec![];
for alias in names {
// Find comments above.
let mut alias_atop = vec![];
while let Some(comment) = comments_iter
.next_if(|comment| comment.location.row() < alias.location.row())
{
alias_atop.push(comment);
}
// Find comments inline.
let mut alias_inline = vec![];
while let Some(comment) = comments_iter.next_if(|comment| {
comment.end_location.row() == alias.end_location.unwrap().row()
}) {
alias_inline.push(comment);
}
aliases.push(AnnotatedAliasData {
name: &alias.node.name,
asname: alias.node.asname.as_ref(),
atop: alias_atop,
inline: alias_inline,
})
}
annotated.push(AnnotatedImport::ImportFrom {
module: module.as_ref(),
names: aliases,
level: level.as_ref(),
atop,
inline,
});
}
_ => unreachable!("Expected StmtKind::Import | StmtKind::ImportFrom"),
}
}
annotated
}
fn normalize_imports(imports: Vec<AnnotatedImport>) -> ImportBlock {
let mut block: ImportBlock = Default::default();
for import in imports {
match import {
AnnotatedImport::Import {
names,
atop,
inline,
} => {
// Associate the comments with the first alias (best effort).
if let Some(name) = names.first() {
let entry = block
.import
.entry(AliasData {
name: name.name,
asname: name.asname,
})
.or_default();
for comment in atop {
entry.atop.push(comment.value);
}
for comment in inline {
entry.inline.push(comment.value);
}
}
// Create an entry for every alias.
for name in &names {
block
.import
.entry(AliasData {
name: name.name,
asname: name.asname,
})
.or_default();
}
}
AnnotatedImport::ImportFrom {
module,
names,
level,
atop,
inline,
} => {
// Associate the comments with the first alias (best effort).
if let Some(alias) = names.first() {
if alias.asname.is_none() {
let entry = &mut block
.import_from
.entry(ImportFromData { module, level })
.or_default()
.insert(AliasData {
name: &name.node.name,
asname: &name.node.asname,
});
.0;
for comment in atop {
entry.atop.push(comment.value);
}
for comment in inline {
entry.inline.push(comment.value);
}
} else {
block.import_from_as.insert((
ImportFromData { module, level },
AliasData {
name: &name.node.name,
asname: &name.node.asname,
},
));
let entry = &mut block
.import_from_as
.entry((
ImportFromData { module, level },
AliasData {
name: alias.name,
asname: alias.asname,
},
))
.or_default();
for comment in atop {
entry.atop.push(comment.value);
}
for comment in inline {
entry.inline.push(comment.value);
}
}
}
// Create an entry for every alias.
for alias in names {
if alias.asname.is_none() {
let entry = block
.import_from
.entry(ImportFromData { module, level })
.or_default()
.1
.entry(AliasData {
name: alias.name,
asname: alias.asname,
})
.or_default();
for comment in alias.atop {
entry.atop.push(comment.value);
}
for comment in alias.inline {
entry.inline.push(comment.value);
}
} else {
let entry = block
.import_from_as
.entry((
ImportFromData { module, level },
AliasData {
name: alias.name,
asname: alias.asname,
},
))
.or_default();
entry
.atop
.extend(alias.atop.into_iter().map(|comment| comment.value));
for comment in alias.inline {
entry.inline.push(comment.value);
}
}
}
}
_ => unreachable!("Expected StmtKind::Import | StmtKind::ImportFrom"),
}
}
block
@@ -73,10 +275,10 @@ fn categorize_imports<'a>(
) -> BTreeMap<ImportType, ImportBlock<'a>> {
let mut block_by_type: BTreeMap<ImportType, ImportBlock> = Default::default();
// Categorize `StmtKind::Import`.
for alias in block.import {
for (alias, comments) in block.import {
let import_type = categorize(
&alias.module_base(),
&None,
None,
src,
known_first_party,
known_third_party,
@@ -86,7 +288,7 @@ fn categorize_imports<'a>(
.entry(import_type)
.or_default()
.import
.insert(alias);
.insert(alias, comments);
}
// Categorize `StmtKind::ImportFrom` (without re-export).
for (import_from, aliases) in block.import_from {
@@ -105,7 +307,7 @@ fn categorize_imports<'a>(
.insert(import_from, aliases);
}
// Categorize `StmtKind::ImportFrom` (with re-export).
for (import_from, alias) in block.import_from_as {
for ((import_from, alias), comments) in block.import_from_as {
let classification = categorize(
&import_from.module_base(),
import_from.level,
@@ -118,7 +320,7 @@ fn categorize_imports<'a>(
.entry(classification)
.or_default()
.import_from_as
.insert((import_from, alias));
.insert((import_from, alias), comments);
}
block_by_type
}
@@ -131,7 +333,7 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
block
.import
.into_iter()
.sorted_by_cached_key(|alias| module_key(alias.name, alias.asname)),
.sorted_by_cached_key(|(alias, _)| module_key(alias.name, alias.asname)),
);
// Sort `StmtKind::ImportFrom`.
@@ -145,29 +347,47 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
block
.import_from_as
.into_iter()
.map(|(import_from, alias)| (import_from, FnvHashSet::from_iter([alias]))),
.map(|((import_from, alias), comments)| {
(
import_from,
(
CommentSet {
atop: comments.atop,
inline: Default::default(),
},
FnvHashMap::from_iter([(
alias,
CommentSet {
atop: Default::default(),
inline: comments.inline,
},
)]),
),
)
}),
)
.map(|(import_from, aliases)| {
.map(|(import_from, (comments, aliases))| {
// Within each `StmtKind::ImportFrom`, sort the members.
(
import_from,
comments,
aliases
.into_iter()
.sorted_by_cached_key(|alias| member_key(alias.name, alias.asname))
.collect::<Vec<AliasData>>(),
.sorted_by_cached_key(|(alias, _)| member_key(alias.name, alias.asname))
.collect::<Vec<(AliasData, CommentSet)>>(),
)
})
.sorted_by_cached_key(|(import_from, aliases)| {
.sorted_by_cached_key(|(import_from, _, aliases)| {
// Sort each `StmtKind::ImportFrom` by module key, breaking ties based on
// members.
(
import_from
.module
.as_ref()
.map(|module| module_key(module, &None)),
.map(|module| module_key(module, None)),
aliases
.first()
.map(|alias| member_key(alias.name, alias.asname)),
.map(|(alias, _)| member_key(alias.name, alias.asname)),
)
}),
);
@@ -176,15 +396,18 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
}
pub fn format_imports(
block: Vec<&Stmt>,
block: &[&Stmt],
comments: Vec<Comment>,
line_length: &usize,
src: &[PathBuf],
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
extra_standard_library: &BTreeSet<String>,
) -> String {
let block = annotate_imports(block, comments);
// Normalize imports (i.e., deduplicate, aggregate `from` imports).
let block = normalize_imports(&block);
let block = normalize_imports(block);
// Categorize by type (e.g., first-party vs. third-party).
let block_by_type = categorize_imports(
@@ -195,81 +418,38 @@ pub fn format_imports(
extra_standard_library,
);
// Generate replacement source code.
let mut output = RopeBuilder::new();
let mut first_block = true;
// Generate replacement source code.
let mut is_first_block = true;
for import_block in block_by_type.into_values() {
let import_block = sort_imports(import_block);
// Add a blank line between every section.
if !first_block {
if !is_first_block {
output.append("\n");
} else {
first_block = false;
is_first_block = false;
}
let mut is_first_statement = true;
// Format `StmtKind::Import` statements.
for AliasData { name, asname } in import_block.import.iter() {
if let Some(asname) = asname {
output.append(&format!("import {} as {}\n", name, asname));
} else {
output.append(&format!("import {}\n", name));
}
for (alias, comments) in import_block.import.iter() {
output.append(&format::format_import(alias, comments, is_first_statement));
is_first_statement = false;
}
// Format `StmtKind::ImportFrom` statements.
for (import_from, aliases) in import_block.import_from.iter() {
let prelude: String = format!("from {} import ", import_from.module_name());
let members: Vec<String> = aliases
.iter()
.map(|AliasData { name, asname }| {
if let Some(asname) = asname {
format!("{} as {}", name, asname)
} else {
name.to_string()
}
})
.collect();
// Can we fit the import on a single line?
let expected_len: usize =
// `from base import `
prelude.len()
// `member( as alias)?`
+ members.iter().map(|part| part.len()).sum::<usize>()
// `, `
+ 2 * (members.len() - 1);
if expected_len <= *line_length {
// `from base import `
output.append(&prelude);
// `member( as alias)?(, )?`
for (index, part) in members.into_iter().enumerate() {
if index > 0 {
output.append(", ");
}
output.append(&part);
}
// `\n`
output.append("\n");
} else {
// `from base import (\n`
output.append(&prelude);
output.append("(");
output.append("\n");
// ` member( as alias)?,\n`
for part in members {
output.append(INDENT);
output.append(&part);
output.append(",");
output.append("\n");
}
// `)\n`
output.append(")");
output.append("\n");
}
for (import_from, comments, aliases) in import_block.import_from.iter() {
output.append(&format::format_import_from(
import_from,
comments,
aliases,
line_length,
is_first_statement,
));
is_first_statement = false;
}
}
output.finish().to_string()
@@ -287,13 +467,17 @@ mod tests {
use crate::linter::test_path;
use crate::Settings;
#[test_case(Path::new("add_newline_before_comments.py"))]
#[test_case(Path::new("combine_import_froms.py"))]
#[test_case(Path::new("comments.py"))]
#[test_case(Path::new("deduplicate_imports.py"))]
#[test_case(Path::new("fit_line_length.py"))]
#[test_case(Path::new("fit_line_length_comment.py"))]
#[test_case(Path::new("import_from_after_import.py"))]
#[test_case(Path::new("leading_prefix.py"))]
#[test_case(Path::new("no_reorder_within_section.py"))]
#[test_case(Path::new("order_by_type.py"))]
#[test_case(Path::new("preserve_comment_order.py"))]
#[test_case(Path::new("preserve_indentation.py"))]
#[test_case(Path::new("reorder_within_section.py"))]
#[test_case(Path::new("separate_first_party_imports.py"))]
@@ -303,6 +487,7 @@ mod tests {
#[test_case(Path::new("skip.py"))]
#[test_case(Path::new("sort_similar_imports.py"))]
#[test_case(Path::new("trailing_suffix.py"))]
#[test_case(Path::new("type_comments.py"))]
fn isort(path: &Path) -> Result<()> {
let snapshot = format!("{}", path.to_string_lossy());
let mut checks = test_path(

View File

@@ -5,7 +5,7 @@ use crate::ast::types::Range;
use crate::autofix::{fixer, Fix};
use crate::checks::CheckKind;
use crate::docstrings::helpers::leading_space;
use crate::isort::format_imports;
use crate::isort::{comments, format_imports};
use crate::{Check, Settings, SourceCodeLocator};
fn extract_range(body: &[&Stmt]) -> Range {
@@ -44,7 +44,15 @@ fn match_trailing_content(body: &[&Stmt], locator: &SourceCodeLocator) -> bool {
end_location: Location::new(end_location.row() + 1, 0),
};
let suffix = locator.slice_source_code_range(&range);
suffix.chars().any(|char| !char.is_whitespace())
for char in suffix.chars() {
if char == '#' {
return false;
}
if !char.is_whitespace() {
return true;
}
}
false
}
/// I001
@@ -57,13 +65,23 @@ pub fn check_imports(
let range = extract_range(&body);
let indentation = extract_indentation(&body, locator);
// Extract comments. Take care to grab any inline comments from the last line.
let comments = comments::collect_comments(
&Range {
location: range.location,
end_location: Location::new(range.end_location.row() + 1, 0),
},
locator,
);
// Special-cases: there's leading or trailing content in the import block.
let has_leading_content = match_leading_content(&body, locator);
let has_trailing_content = match_trailing_content(&body, locator);
// Generate the sorted import block.
let expected = format_imports(
body,
&body,
comments,
&(settings.line_length - indentation.len()),
&settings.src,
&settings.isort.known_first_party,

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 8
column: 0
fix:
patch:
content: "import os\n\n# This is a comment in the same section, so we need to add one newline.\nimport sys\n\nimport numpy as np\n\n# This is a comment, but it starts a new section, so we don't need to add a newline\n# before it.\nimport leading_prefix\n"
location:
row: 1
column: 0
end_location:
row: 8
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 3
column: 0
end_location:
row: 26
column: 0
fix:
patch:
content: "import B # Comment 4\n\n# Comment 3a\n# Comment 3b\nimport C\nimport D\n\n# Comment 5\n# Comment 6\nfrom A import (\n a, # Comment 7 # Comment 9\n b, # Comment 10\n c, # Comment 8 # Comment 11\n)\n"
location:
row: 3
column: 0
end_location:
row: 26
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 5
column: 0
fix:
patch:
content: "import a\n\n# Don't take this comment into account when determining whether the next import can fit on one line.\nfrom b import c\nfrom d import ( # Do take this comment into account when determining whether the next import can fit on one line.\n e,\n)\n"
location:
row: 1
column: 0
end_location:
row: 5
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 12
column: 0
fix:
patch:
content: "import abc\nimport io\n\n# Old MacDonald had a farm,\n# EIEIO\n# And on his farm he had a cow,\n# EIEIO\n# With a moo-moo here and a moo-moo there\n# Here a moo, there a moo, everywhere moo-moo\n# Old MacDonald had a farm,\n# EIEIO\nfrom errno import EIO\n"
location:
row: 1
column: 0
end_location:
row: 12
column: 0
applied: false

View File

@@ -0,0 +1,6 @@
---
source: src/isort/mod.rs
expression: checks
---
[]

View File

@@ -10,15 +10,15 @@ pub enum Prefix {
pub fn module_key<'a>(
name: &'a str,
asname: &'a Option<String>,
) -> (String, &'a str, &'a Option<String>) {
asname: Option<&'a String>,
) -> (String, &'a str, Option<&'a String>) {
(name.to_lowercase(), name, asname)
}
pub fn member_key<'a>(
name: &'a str,
asname: &'a Option<String>,
) -> (Prefix, String, &'a Option<String>) {
asname: Option<&'a String>,
) -> (Prefix, String, Option<&'a String>) {
(
if name.len() > 1 && string::is_upper(name) {
// Ex) `CONSTANT`

View File

@@ -1,15 +1,25 @@
use fnv::{FnvHashMap, FnvHashSet};
use std::borrow::Cow;
use fnv::FnvHashMap;
use crate::ast;
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub struct ImportFromData<'a> {
pub module: &'a Option<String>,
pub level: &'a Option<usize>,
pub module: Option<&'a String>,
pub level: Option<&'a usize>,
}
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub struct AliasData<'a> {
pub name: &'a str,
pub asname: &'a Option<String>,
pub asname: Option<&'a String>,
}
#[derive(Debug, Default)]
pub struct CommentSet<'a> {
pub atop: Vec<Cow<'a, str>>,
pub inline: Vec<Cow<'a, str>>,
}
pub trait Importable {
@@ -29,16 +39,7 @@ impl Importable for AliasData<'_> {
impl Importable for ImportFromData<'_> {
fn module_name(&self) -> String {
let mut module_name = String::new();
if let Some(level) = self.level {
if level > &0 {
module_name.push_str(&".".repeat(*level));
}
}
if let Some(module) = self.module {
module_name.push_str(module);
}
module_name
ast::helpers::format_import_from(self.level, self.module)
}
fn module_base(&self) -> String {
@@ -50,17 +51,24 @@ impl Importable for ImportFromData<'_> {
pub struct ImportBlock<'a> {
// Set of (name, asname), used to track regular imports.
// Ex) `import module`
pub import: FnvHashSet<AliasData<'a>>,
pub import: FnvHashMap<AliasData<'a>, CommentSet<'a>>,
// Map from (module, level) to `AliasData`, used to track 'from' imports.
// Ex) `from module import member`
pub import_from: FnvHashMap<ImportFromData<'a>, FnvHashSet<AliasData<'a>>>,
pub import_from:
FnvHashMap<ImportFromData<'a>, (CommentSet<'a>, FnvHashMap<AliasData<'a>, CommentSet<'a>>)>,
// Set of (module, level, name, asname), used to track re-exported 'from' imports.
// Ex) `from module import member as member`
pub import_from_as: FnvHashSet<(ImportFromData<'a>, AliasData<'a>)>,
pub import_from_as: FnvHashMap<(ImportFromData<'a>, AliasData<'a>), CommentSet<'a>>,
}
type AliasDataWithComments<'a> = (AliasData<'a>, CommentSet<'a>);
#[derive(Debug, Default)]
pub struct OrderedImportBlock<'a> {
pub import: Vec<AliasData<'a>>,
pub import_from: Vec<(ImportFromData<'a>, Vec<AliasData<'a>>)>,
pub import: Vec<AliasDataWithComments<'a>>,
pub import_from: Vec<(
ImportFromData<'a>,
CommentSet<'a>,
Vec<AliasDataWithComments<'a>>,
)>,
}

View File

@@ -52,6 +52,8 @@ mod pyupgrade;
mod rules;
pub mod settings;
pub mod source_code_locator;
#[cfg(feature = "update-informer")]
pub mod updates;
pub mod visibility;
/// Run Ruff over Python source code directly.
@@ -62,14 +64,16 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
let pyproject = pyproject::find_pyproject_toml(&project_root);
let pyproject = pyproject::find_pyproject_toml(project_root.as_ref());
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let settings =
Settings::from_configuration(Configuration::from_pyproject(&pyproject, &project_root)?);
let settings = Settings::from_configuration(Configuration::from_pyproject(
pyproject.as_ref(),
project_root.as_ref(),
)?);
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);

View File

@@ -344,6 +344,7 @@ mod tests {
#[test_case(CheckCode::B017, Path::new("B017.py"); "B017")]
#[test_case(CheckCode::B018, Path::new("B018.py"); "B018")]
#[test_case(CheckCode::B019, Path::new("B019.py"); "B019")]
#[test_case(CheckCode::B020, Path::new("B020.py"); "B020")]
#[test_case(CheckCode::B021, Path::new("B021.py"); "B021")]
#[test_case(CheckCode::B022, Path::new("B022.py"); "B022")]
#[test_case(CheckCode::B024, Path::new("B024.py"); "B024")]
@@ -456,6 +457,7 @@ mod tests {
#[test_case(CheckCode::F821, Path::new("F821_2.py"); "F821_2")]
#[test_case(CheckCode::F821, Path::new("F821_3.py"); "F821_3")]
#[test_case(CheckCode::F821, Path::new("F821_4.py"); "F821_4")]
#[test_case(CheckCode::F821, Path::new("F821_5.py"); "F821_5")]
#[test_case(CheckCode::F822, Path::new("F822.py"); "F822")]
#[test_case(CheckCode::F823, Path::new("F823.py"); "F823")]
#[test_case(CheckCode::F831, Path::new("F831.py"); "F831")]

View File

@@ -17,6 +17,8 @@ use ::ruff::settings::configuration::Configuration;
use ::ruff::settings::types::FilePattern;
use ::ruff::settings::user::UserConfiguration;
use ::ruff::settings::{pyproject, Settings};
#[cfg(feature = "update-informer")]
use ::ruff::updates;
use anyhow::Result;
use clap::Parser;
use colored::Colorize;
@@ -26,11 +28,6 @@ use notify::{raw_watcher, RecursiveMode, Watcher};
use rayon::prelude::*;
use walkdir::DirEntry;
#[cfg(feature = "update-informer")]
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
#[cfg(feature = "update-informer")]
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Shim that calls par_iter except for wasm because there's no wasm support in
/// rayon yet (there is a shim to be used for the web, but it requires js
/// cooperation) Unfortunately, ParallelIterator does not implement Iterator so
@@ -45,30 +42,6 @@ fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl Iterator<Item = &T> {
iterable.iter()
}
#[cfg(feature = "update-informer")]
fn check_for_updates() {
use update_informer::{registry, Check};
let informer = update_informer::new(registry::PyPI, CARGO_PKG_NAME, CARGO_PKG_VERSION);
if let Some(new_version) = informer.check_version().ok().flatten() {
let msg = format!(
"A new version of {pkg_name} is available: v{pkg_version} -> {new_version}",
pkg_name = CARGO_PKG_NAME.italic().cyan(),
pkg_version = CARGO_PKG_VERSION,
new_version = new_version.to_string().green()
);
let cmd = format!(
"Run to update: {cmd} {pkg_name}",
cmd = "pip3 install --upgrade".green(),
pkg_name = CARGO_PKG_NAME.green()
);
println!("\n{msg}\n{cmd}");
}
}
fn show_settings(
configuration: Configuration,
project_root: Option<PathBuf>,
@@ -235,7 +208,7 @@ fn inner_main() -> Result<ExitCode> {
};
let pyproject = cli
.config
.or_else(|| pyproject::find_pyproject_toml(&project_root));
.or_else(|| pyproject::find_pyproject_toml(project_root.as_ref()));
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
@@ -245,15 +218,16 @@ fn inner_main() -> Result<ExitCode> {
let exclude: Vec<FilePattern> = cli
.exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.map(|path| FilePattern::from_user(path, project_root.as_ref()))
.collect();
let extend_exclude: Vec<FilePattern> = cli
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.map(|path| FilePattern::from_user(path, project_root.as_ref()))
.collect();
let mut configuration = Configuration::from_pyproject(&pyproject, &project_root)?;
let mut configuration =
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?;
if !exclude.is_empty() {
configuration.exclude = exclude;
}
@@ -262,7 +236,7 @@ fn inner_main() -> Result<ExitCode> {
}
if !cli.per_file_ignores.is_empty() {
configuration.per_file_ignores =
collect_per_file_ignores(cli.per_file_ignores, &project_root);
collect_per_file_ignores(cli.per_file_ignores, project_root.as_ref());
}
if !cli.select.is_empty() {
warn_on(
@@ -271,7 +245,7 @@ fn inner_main() -> Result<ExitCode> {
&cli.ignore,
&cli.extend_ignore,
&configuration,
&pyproject,
pyproject.as_ref(),
);
configuration.select = cli.select;
}
@@ -282,7 +256,7 @@ fn inner_main() -> Result<ExitCode> {
&cli.ignore,
&cli.extend_ignore,
&configuration,
&pyproject,
pyproject.as_ref(),
);
configuration.extend_select = cli.extend_select;
}
@@ -292,6 +266,9 @@ fn inner_main() -> Result<ExitCode> {
if !cli.extend_ignore.is_empty() {
configuration.extend_ignore = cli.extend_ignore;
}
if let Some(line_length) = cli.line_length {
configuration.line_length = line_length;
}
if let Some(target_version) = cli.target_version {
configuration.target_version = target_version;
}
@@ -402,8 +379,8 @@ fn inner_main() -> Result<ExitCode> {
// Check for updates if we're in a non-silent log level.
#[cfg(feature = "update-informer")]
if !is_stdin && log_level >= LogLevel::Default {
check_for_updates();
if !is_stdin && log_level >= LogLevel::Default && atty::is(atty::Stream::Stdout) {
let _ = updates::check_for_updates();
}
if messages.iter().any(|message| !message.fixed) && !cli.exit_zero {

View File

@@ -72,10 +72,7 @@ fn add_noqa_inner(
// Grab the noqa (logical) line number for the current (physical) line.
// If there are newlines at the end of the file, they won't be represented in
// `noqa_line_for`, so fallback to the current line.
let noqa_lineno = noqa_line_for
.get(&lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
if !codes.is_empty() {
let matches = matches_by_line.entry(noqa_lineno).or_default();

View File

@@ -1,3 +1,4 @@
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Arguments, Expr, ExprKind, Stmt};
use crate::ast::types::{Range, Scope, ScopeKind};
@@ -58,10 +59,19 @@ pub fn invalid_first_argument_name_for_class_method(
name: &str,
decorator_list: &[Expr],
args: &Arguments,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
settings: &Settings,
) -> Option<Check> {
if matches!(
helpers::function_type(scope, name, decorator_list, settings),
helpers::function_type(
scope,
name,
decorator_list,
from_imports,
import_aliases,
settings,
),
FunctionType::ClassMethod
) {
if let Some(arg) = args.args.first() {
@@ -82,10 +92,19 @@ pub fn invalid_first_argument_name_for_method(
name: &str,
decorator_list: &[Expr],
args: &Arguments,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
settings: &Settings,
) -> Option<Check> {
if matches!(
helpers::function_type(scope, name, decorator_list, settings),
helpers::function_type(
scope,
name,
decorator_list,
from_imports,
import_aliases,
settings,
),
FunctionType::Method
) {
if let Some(arg) = args.args.first() {

View File

@@ -1,14 +1,16 @@
use fnv::{FnvHashMap, FnvHashSet};
use itertools::Itertools;
use rustpython_ast::{Expr, ExprKind, Stmt, StmtKind};
use rustpython_ast::{Expr, Stmt, StmtKind};
use crate::ast::helpers::{collect_call_paths, match_call_path, match_name_or_attr};
use crate::ast::helpers::{
collect_call_paths, dealias_call_path, match_call_path, to_module_and_member,
};
use crate::ast::types::{Scope, ScopeKind};
use crate::pep8_naming::settings::Settings;
use crate::python::string::{is_lower, is_upper};
const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"];
const METACLASS_BASES: [&str; 2] = ["type", "ABCMeta"];
const METACLASS_BASES: [(&str, &str); 2] = [("", "type"), ("abc", "ABCMeta")];
pub enum FunctionType {
Function,
@@ -22,35 +24,39 @@ pub fn function_type(
scope: &Scope,
name: &str,
decorator_list: &[Expr],
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
import_aliases: &FnvHashMap<&str, &str>,
settings: &Settings,
) -> FunctionType {
if let ScopeKind::Class(scope) = &scope.kind {
// Special-case class method, like `__new__`.
if CLASS_METHODS.contains(&name)
// The class itself extends a known metaclass, so all methods are class methods.
|| scope.bases.iter().any(|expr| {
METACLASS_BASES
.iter()
.any(|target| match_name_or_attr(expr, target))
// The class itself extends a known metaclass, so all methods are class methods.
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
METACLASS_BASES.iter().any(|(module, member)| {
match_call_path(&call_path, module, member, from_imports)
})
})
// The method is decorated with a class method decorator (like `@classmethod`).
|| decorator_list.iter().any(|expr| {
if let ExprKind::Name { id, .. } = &expr.node {
settings.classmethod_decorators.contains(id)
} else {
false
}
}) {
// The method is decorated with a class method decorator (like `@classmethod`).
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
settings.classmethod_decorators.iter().any(|decorator| {
let (module, member) = to_module_and_member(decorator);
match_call_path(&call_path, module, member, from_imports)
})
})
{
FunctionType::ClassMethod
} else if decorator_list.iter().any(|expr| {
if let ExprKind::Name { id, .. } = &expr.node {
settings.staticmethod_decorators.contains(id)
} else {
false
}
}) {
// The method is decorated with a static method decorator (like
// `@staticmethod`).
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
settings.staticmethod_decorators.iter().any(|decorator| {
let (module, member) = to_module_and_member(decorator);
match_call_path(&call_path, module, member, from_imports)
})
}) {
FunctionType::StaticMethod
} else {
// It's an instance method.

View File

@@ -1,6 +1,6 @@
use itertools::izip;
use rustpython_ast::Location;
use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Unaryop};
use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Stmt, Unaryop};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind, RejectedCmpop};
@@ -47,12 +47,16 @@ pub fn ambiguous_function_name(name: &str, location: Range) -> Option<Check> {
}
/// E731
pub fn do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Lambda { .. } = &value.node {
Some(Check::new(CheckKind::DoNotAssignLambda, location))
} else {
None
pub fn do_not_assign_lambda(target: &Expr, value: &Expr, stmt: &Stmt) -> Option<Check> {
if let ExprKind::Name { .. } = &target.node {
if let ExprKind::Lambda { .. } = &value.node {
return Some(Check::new(
CheckKind::DoNotAssignLambda,
Range::from_located(stmt),
));
}
}
None
}
/// E713, E714

View File

@@ -64,9 +64,9 @@ pub fn unused_variables(scope: &Scope, dummy_variable_rgx: &Regex) -> Vec<Check>
if binding.used.is_none()
&& matches!(binding.kind, BindingKind::Assignment)
&& !dummy_variable_rgx.is_match(name)
&& name != "__tracebackhide__"
&& name != "__traceback_info__"
&& name != "__traceback_supplement__"
&& name != &"__tracebackhide__"
&& name != &"__traceback_info__"
&& name != &"__traceback_supplement__"
{
checks.push(Check::new(
CheckKind::UnusedVariable(name.to_string()),

View File

@@ -115,7 +115,7 @@ pub fn useless_object_inheritance(name: &str, bases: &[Expr], scope: &Scope) ->
for expr in bases {
if let ExprKind::Name { id, .. } = &expr.node {
if id == "object" {
match scope.values.get(id) {
match scope.values.get(&id.as_str()) {
None
| Some(Binding {
kind: BindingKind::Builtin,

View File

@@ -1,6 +1,6 @@
use rustpython_ast::{Constant, Expr, ExprKind, Operator};
use crate::ast::helpers::collect_call_paths;
use crate::ast::helpers::{collect_call_paths, dealias_call_path};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::check_ast::Checker;
@@ -44,8 +44,8 @@ fn union(elts: &[Expr]) -> Expr {
/// U007
pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, slice: &Expr) {
let call_path = collect_call_paths(value);
if checker.match_typing_module(&call_path, "Optional") {
let call_path = dealias_call_path(collect_call_paths(value), &checker.import_aliases);
if checker.match_typing_call_path(&call_path, "Optional") {
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
if checker.patch() {
let mut generator = SourceGenerator::new();
@@ -60,7 +60,7 @@ pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, s
}
}
checker.add_check(check);
} else if checker.match_typing_module(&call_path, "Union") {
} else if checker.match_typing_call_path(&call_path, "Union") {
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
if checker.patch() {
match &slice.node {

View File

@@ -65,8 +65,8 @@ static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
impl Configuration {
pub fn from_pyproject(
pyproject: &Option<PathBuf>,
project_root: &Option<PathBuf>,
pyproject: Option<&PathBuf>,
project_root: Option<&PathBuf>,
) -> Result<Self> {
let options = load_options(pyproject)?;
Ok(Configuration {

View File

@@ -36,7 +36,7 @@ fn parse_pyproject_toml(path: &Path) -> Result<Pyproject> {
toml::from_str(&contents).map_err(|e| e.into())
}
pub fn find_pyproject_toml(path: &Option<PathBuf>) -> Option<PathBuf> {
pub fn find_pyproject_toml(path: Option<&PathBuf>) -> Option<PathBuf> {
if let Some(path) = path {
let path_pyproject_toml = path.join("pyproject.toml");
if path_pyproject_toml.is_file() {
@@ -80,7 +80,7 @@ pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
None
}
pub fn load_options(pyproject: &Option<PathBuf>) -> Result<Options> {
pub fn load_options(pyproject: Option<&PathBuf>) -> Result<Options> {
match pyproject {
Some(pyproject) => Ok(parse_pyproject_toml(pyproject)?
.tool
@@ -323,7 +323,7 @@ other-attribute = 1
assert_eq!(project_root, cwd.join("resources/test/fixtures"));
let path =
find_pyproject_toml(&Some(project_root)).expect("Unable to find pyproject.toml.");
find_pyproject_toml(Some(&project_root)).expect("Unable to find pyproject.toml.");
assert_eq!(path, cwd.join("resources/test/fixtures/pyproject.toml"));
let pyproject = parse_pyproject_toml(&path)?;

View File

@@ -51,7 +51,7 @@ pub enum FilePattern {
}
impl FilePattern {
pub fn from_user(pattern: &str, project_root: &Option<PathBuf>) -> Self {
pub fn from_user(pattern: &str, project_root: Option<&PathBuf>) -> Self {
let path = Path::new(pattern);
let absolute_path = match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
@@ -79,7 +79,7 @@ impl PerFileIgnore {
pub fn new(
pattern: &str,
prefixes: &[CheckCodePrefix],
project_root: &Option<PathBuf>,
project_root: Option<&PathBuf>,
) -> Self {
let pattern = FilePattern::from_user(pattern, project_root);
let codes = BTreeSet::from_iter(prefixes.iter().flat_map(|prefix| prefix.codes()));

View File

@@ -0,0 +1,32 @@
---
source: src/linter.rs
expression: checks
---
- kind:
LoopVariableOverridesIterator: items
location:
row: 8
column: 4
end_location:
row: 8
column: 9
fix: ~
- kind:
LoopVariableOverridesIterator: values
location:
row: 21
column: 9
end_location:
row: 21
column: 15
fix: ~
- kind:
LoopVariableOverridesIterator: vars
location:
row: 36
column: 4
end_location:
row: 36
column: 8
fix: ~

View File

@@ -26,20 +26,4 @@ expression: checks
row: 7
column: 29
fix: ~
- kind: DoNotAssignLambda
location:
row: 12
column: 0
end_location:
row: 12
column: 27
fix: ~
- kind: DoNotAssignLambda
location:
row: 16
column: 0
end_location:
row: 16
column: 25
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UndefinedName: InnerClass
location:
row: 5
column: 29
end_location:
row: 5
column: 41
fix: ~

View File

@@ -4,18 +4,34 @@ expression: checks
---
- kind: InvalidFirstArgumentNameForMethod
location:
row: 5
row: 7
column: 19
end_location:
row: 5
row: 7
column: 23
fix: ~
- kind: InvalidFirstArgumentNameForMethod
location:
row: 10
row: 12
column: 29
end_location:
row: 10
row: 12
column: 33
fix: ~
- kind: InvalidFirstArgumentNameForMethod
location:
row: 27
column: 14
end_location:
row: 27
column: 17
fix: ~
- kind: InvalidFirstArgumentNameForMethod
location:
row: 31
column: 14
end_location:
row: 31
column: 17
fix: ~

75
src/updates.rs Normal file
View File

@@ -0,0 +1,75 @@
use std::fs::{create_dir_all, read_to_string, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::Result;
use colored::Colorize;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
fn cache_dir() -> &'static str {
"./.ruff_cache"
}
fn file_path() -> PathBuf {
Path::new(cache_dir()).join(".update-informer")
}
/// Get the "latest" version for which the user has been informed.
fn get_latest() -> Result<Option<String>> {
let path = file_path();
if path.exists() {
Ok(Some(read_to_string(path)?.trim().to_string()))
} else {
Ok(None)
}
}
/// Set the "latest" version for which the user has been informed.
fn set_latest(version: &str) -> Result<()> {
create_dir_all(cache_dir())?;
let path = file_path();
let mut file = File::create(path)?;
file.write_all(version.trim().as_bytes())
.map_err(|e| e.into())
}
/// Update the user if a newer version is available.
pub fn check_for_updates() -> Result<()> {
use update_informer::{registry, Check};
let informer = update_informer::new(registry::PyPI, CARGO_PKG_NAME, CARGO_PKG_VERSION);
if let Some(new_version) = informer
.check_version()
.ok()
.flatten()
.map(|version| version.to_string())
{
// If we've already notified the user about this version, return early.
if let Some(latest_version) = get_latest()? {
if latest_version == new_version {
return Ok(());
}
}
set_latest(&new_version)?;
let msg = format!(
"A new version of {pkg_name} is available: v{pkg_version} -> {new_version}",
pkg_name = CARGO_PKG_NAME.italic().cyan(),
pkg_version = CARGO_PKG_VERSION,
new_version = new_version.green()
);
let cmd = format!(
"Run to update: {cmd} {pkg_name}",
cmd = "pip3 install --upgrade".green(),
pkg_name = CARGO_PKG_NAME.green()
);
println!("\n{msg}\n{cmd}");
}
Ok(())
}