Compare commits

...

15 Commits

Author SHA1 Message Date
Charlie Marsh
7f77ed0f86 Bump version to 0.0.105 2022-11-06 21:17:00 -05:00
Charlie Marsh
1b33cfb9cb Respect project root in per-file ignores (#631) 2022-11-06 21:15:49 -05:00
Charlie Marsh
dbc64f1faa Remove erroneous Literal entry from subscript list (#630) 2022-11-06 21:03:41 -05:00
Charlie Marsh
f4b5f0d259 Remove foo.py 2022-11-06 21:03:23 -05:00
Charlie Marsh
85b882fc54 Remove CheckLocator abstraction (#627) 2022-11-06 17:42:10 -05:00
Charlie Marsh
99d9aa61bf Implement flake8-annotations (#625) 2022-11-06 17:25:49 -05:00
Charlie Marsh
050f34dd25 Bump version to 0.0.104 2022-11-06 15:31:10 -05:00
Charlie Marsh
1cd82d588b Categorize functions in pep8-naming (#624) 2022-11-06 15:29:49 -05:00
Charlie Marsh
cea9e34942 Update CONTRIBUTING.md (#623) 2022-11-06 14:31:16 -05:00
Reiner Gerecke
1ede377402 Improve discoverability of dev commands (#621) 2022-11-06 14:25:59 -05:00
Reiner Gerecke
82eff641fb Remove utf-8 encoding declaration (#618) 2022-11-06 14:23:06 -05:00
Charlie Marsh
eb1bc9f092 Allow underscore names in N803 (#622) 2022-11-06 14:19:02 -05:00
Reiner Gerecke
df88504dea pyflakes F632 Autofix (#612) 2022-11-06 06:57:46 -05:00
Anders Kaseorg
b9ec3e9137 Correct source link in CONFUSABLES comment (#617) 2022-11-05 20:53:05 -04:00
Anders Kaseorg
b067b665ff Fix B015 false positive on comparison deep inside expression statement (#616) 2022-11-05 20:13:22 -04:00
120 changed files with 3046 additions and 882 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[alias]
dev = "run --package ruff_dev --bin ruff_dev"

View File

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

View File

@@ -1,17 +1,17 @@
# Contributing to ruff
# Contributing to Ruff
Welcome! We're happy to have you here. Thank you in advance for your contribution to ruff.
Welcome! We're happy to have you here. Thank you in advance for your contribution to Ruff.
## The basics
ruff welcomes contributions in the form of Pull Requests. For small changes (e.g., bug fixes), feel
Ruff welcomes contributions in the form of Pull Requests. For small changes (e.g., bug fixes), feel
free to submit a PR. For larger changes (e.g., new lint rules, new functionality, new configuration
options), consider submitting an [Issue](https://github.com/charliermarsh/ruff/issues) outlining
your proposed change.
### Prerequisites
ruff is written in Rust. You'll need to install the
Ruff is written in Rust. You'll need to install the
[Rust toolchain](https://www.rust-lang.org/tools/install) for development.
You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests:
@@ -22,7 +22,7 @@ cargo install cargo-insta
### Development
After cloning the repository, run ruff locally with:
After cloning the repository, run Ruff locally with:
```shell
cargo run resources/test/fixtures --no-cache
@@ -32,9 +32,9 @@ Prior to opening a pull request, ensure that your code has been auto-formatted,
both the lint and test validation checks:
```shell
cargo fmt # Auto-formatting...
cargo clippy # Linting...
cargo test # Testing...
cargo +nightly fmt --all # Auto-formatting...
cargo +nightly clippy --all # Linting...
cargo +nightly test --all # Testing...
```
These checks will run on GitHub Actions when you open your Pull Request, but running them locally
@@ -45,12 +45,13 @@ prior to merging.
### Example: Adding a new lint rule
There are three phases to adding a new lint rule:
There are four phases to adding a new lint rule:
1. Define the rule in `src/checks.rs`.
2. Define the _logic_ for triggering the rule in `src/check_ast.rs` (for AST-based checks)
or `src/check_lines.rs` (for text-based checks).
2. Define the _logic_ for triggering the rule in `src/check_ast.rs` (for AST-based checks),
`src/check_tokens.rs` (for token-based checks), or `src/check_lines.rs` (for text-based checks).
3. Add a test fixture.
4. Update the generated files (documentation and generated code).
To define the rule, open up `src/checks.rs`. You'll need to define both a `CheckCode` and
`CheckKind`. As an example, you can grep for `E402` and `ModuleImportNotAtTopOfFile`, and follow the
@@ -58,37 +59,33 @@ pattern implemented therein.
To trigger the rule, you'll likely want to augment the logic in `src/check_ast.rs`, which defines
the Python AST visitor, responsible for iterating over the abstract syntax tree and collecting
lint-rule violations as it goes. Grep for the `Check::new` invocations to understand how other,
similar rules are implemented.
lint-rule violations as it goes. If you need to inspect the AST, you can run `cargo dev print-ast`
with a Python file. Grep for the `Check::new` invocations to understand how other, similar rules
are implemented.
To add a test fixture, create a file under `resources/test/fixtures`, named to match the `CheckCode`
you defined earlier (e.g., `E402.py`). This file should contain a variety of violations and
non-violations designed to evaluate and demonstrate the behavior of your lint rule. Run ruff locally
with (e.g.) `cargo run resources/test/fixtures/E402.py`. Once you're satisfied with the output,
codify the behavior as a snapshot test by adding a new function to the `mod tests` section of
`src/linter.rs`, like so:
non-violations designed to evaluate and demonstrate the behavior of your lint rule. Run Ruff locally
with (e.g.) `cargo run resources/test/fixtures/E402.py --no-cache`. Once you're satisfied with the
output, codify the behavior as a snapshot test by adding a new `testcase` macro to the `mod tests`
section of `src/linter.rs`, like so:
```rust
#[test]
fn e402() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E402.py"),
&settings::Settings::for_rule(CheckCode::E402),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test_case(CheckCode::A001, Path::new("A001.py"); "A001")]
...
```
Then, run `cargo test`. Your test will fail, but you'll be prompted to follow-up with
`cargo insta review`. Accept the generated snapshot, then commit the snapshot file alongside the
rest of your changes.
Finally, to update the documentation, run `cargo dev generate-rules-table` from the repo root. To
update the generated prefix map, run `cargo dev generate-check-code-prefix`. Both of these commands
should be run whenever a new check is added to the codebase.
### Example: Adding a new configuration option
ruff's user-facing settings live in two places: first, the command-line options defined with
Ruff's user-facing settings live in two places: first, the command-line options defined with
[clap](https://docs.rs/clap/latest/clap/) via the `Cli` struct in `src/main.rs`; and second, the
`Config` struct defined `src/pyproject.rs`, which is responsible for extracting user-defined
settings from a `pyproject.toml` file.
@@ -103,9 +100,9 @@ acceptable unused variables (e.g., `_`).
## Release process
As of now, ruff has an ad hoc release process: releases are cut with high frequency via GitHub
As of now, Ruff has an ad hoc release process: releases are cut with high frequency via GitHub
Actions, which automatically generates the appropriate wheels across architectures and publishes
them to [PyPI](https://pypi.org/project/ruff/).
ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
Ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4).

6
Cargo.lock generated
View File

@@ -920,7 +920,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.103-dev.0"
version = "0.0.105-dev.0"
dependencies = [
"anyhow",
"clap 4.0.15",
@@ -2221,7 +2221,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.103"
version = "0.0.105"
dependencies = [
"anyhow",
"assert_cmd",
@@ -2266,7 +2266,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.103"
version = "0.0.105"
dependencies = [
"anyhow",
"clap 4.0.15",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.103"
version = "0.0.105"
edition = "2021"
[lib]

View File

@@ -52,8 +52,9 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
8. [flake8-builtins](#flake8-builtins)
9. [flake8-print](#flake8-print)
10. [flake8-quotes](#flake8-quotes)
11. [Ruff-specific rules](#ruff-specific-rules)
12. [Meta rules](#meta-rules)
11. [flake8-annotations](#flake8-annotations)
12. [Ruff-specific rules](#ruff-specific-rules)
13. [Meta rules](#meta-rules)
5. [Editor Integrations](#editor-integrations)
6. [FAQ](#faq)
7. [Development](#development)
@@ -93,7 +94,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.103
rev: v0.0.105
hooks:
- id: ruff
```
@@ -300,7 +301,7 @@ By default, Ruff enables all `E` and `F` error codes, which correspond to those
The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` command-line option.
<!-- Sections automatically generated by examples/generate_rules_table.rs. -->
<!-- Sections automatically generated by `cargo dev generate-rules-table`. -->
<!-- Begin auto-generated sections. -->
### Pyflakes
@@ -322,7 +323,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment | |
| F622 | TwoStarredExpressions | Two starred expressions in assignment | |
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | |
| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | |
| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | 🛠 |
| F633 | InvalidPrintSyntax | Use of `>>` is invalid with `print` function | |
| F634 | IfTuple | If test is a tuple, which is always `True` | |
| F701 | BreakOutsideLoop | `break` outside loop | |
@@ -426,6 +427,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | 🛠 |
| U007 | UsePEP604Annotation | Use `X \| Y` for type annotations | 🛠 |
| U008 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | 🛠 |
| U009 | PEP3120UnnecessaryCodingComment | utf-8 encoding declaration is unnecessary | 🛠 |
### pep8-naming
@@ -522,6 +524,23 @@ For more, see [flake8-quotes](https://pypi.org/project/flake8-quotes/3.3.1/) on
| Q002 | BadQuotesDocstring | Single quote docstring found but double quotes preferred | |
| Q003 | AvoidQuoteEscape | Change outer quotes to avoid escaping inner quotes | |
### flake8-annotations
For more, see [flake8-annotations](https://pypi.org/project/flake8-annotations/2.9.1/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| ANN001 | MissingTypeFunctionArgument | Missing type annotation for function argument | |
| ANN002 | MissingTypeArgs | Missing type annotation for `*args` | |
| ANN003 | MissingTypeKwargs | Missing type annotation for `**kwargs` | |
| ANN101 | MissingTypeSelf | Missing type annotation for `self` in method | |
| ANN102 | MissingTypeCls | Missing type annotation for `cls` in classmethod | |
| ANN201 | MissingReturnTypePublicFunction | Missing return type annotation for public function | |
| ANN202 | MissingReturnTypePrivateFunction | Missing return type annotation for private function | |
| ANN204 | MissingReturnTypeMagicMethod | Missing return type annotation for magic method | |
| ANN205 | MissingReturnTypeStaticMethod | Missing return type annotation for staticmethod | |
| ANN206 | MissingReturnTypeClassMethod | Missing return type annotation for classmethod | |
### Ruff-specific rules
| Code | Name | Message | Fix |
@@ -604,9 +623,10 @@ including:
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (15/32)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (10/34)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (11/34)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
Beyond rule-set parity, Ruff suffers from the following limitations vis-à-vis Flake8:
@@ -626,11 +646,12 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (15/32)
Ruff also implements the functionality that you get from [`yesqa`](https://github.com/asottile/yesqa),
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (10/34).
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (11/34).
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue.

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ use anyhow::Result;
use ruff::flake8_quotes::settings::Quote;
use ruff::settings::options::Options;
use ruff::settings::pyproject::Pyproject;
use ruff::{flake8_quotes, pep8_naming};
use ruff::{flake8_annotations, flake8_quotes, pep8_naming};
use crate::plugin::Plugin;
use crate::{parser, plugin};
@@ -39,6 +39,7 @@ pub fn convert(
// Parse each supported option.
let mut options: Options = Default::default();
let mut flake8_annotations: flake8_annotations::settings::Options = Default::default();
let mut flake8_quotes: flake8_quotes::settings::Options = Default::default();
let mut pep8_naming: pep8_naming::settings::Options = Default::default();
for (key, value) in flake8 {
@@ -78,6 +79,25 @@ pub fn convert(
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
}
}
// flake8-annotations
"suppress-none-returning" | "suppress_none_returning" => {
match parser::parse_bool(value.as_ref()) {
Ok(bool) => flake8_annotations.suppress_none_returning = Some(bool),
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
}
}
"suppress-dummy-args" | "suppress_dummy_args" => {
match parser::parse_bool(value.as_ref()) {
Ok(bool) => flake8_annotations.suppress_dummy_args = Some(bool),
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
}
}
"mypy-init-return" | "mypy_init_return" => {
match parser::parse_bool(value.as_ref()) {
Ok(bool) => flake8_annotations.mypy_init_return = Some(bool),
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
}
}
// flake8-quotes
"quotes" | "inline-quotes" | "inline_quotes" => match value.trim() {
"'" | "single" => flake8_quotes.inline_quotes = Some(Quote::Single),
@@ -94,10 +114,9 @@ pub fn convert(
"\"" | "double" => flake8_quotes.docstring_quotes = Some(Quote::Single),
_ => eprintln!("Unexpected '{key}' value: {value}"),
},
"avoid-escape" | "avoid_escape" => match value.trim() {
"true" => flake8_quotes.avoid_escape = Some(true),
"false" => flake8_quotes.avoid_escape = Some(false),
_ => eprintln!("Unexpected '{key}' value: {value}"),
"avoid-escape" | "avoid_escape" => match parser::parse_bool(value.as_ref()) {
Ok(bool) => flake8_quotes.avoid_escape = Some(bool),
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
},
// pep8-naming
"ignore-names" | "ignore_names" => {
@@ -166,6 +185,7 @@ mod tests {
per_file_ignores: None,
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
pep8_naming: None,
});
@@ -195,6 +215,7 @@ mod tests {
per_file_ignores: None,
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
pep8_naming: None,
});
@@ -224,6 +245,7 @@ mod tests {
per_file_ignores: None,
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
pep8_naming: None,
});
@@ -253,6 +275,7 @@ mod tests {
per_file_ignores: None,
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
pep8_naming: None,
});
@@ -282,6 +305,7 @@ mod tests {
per_file_ignores: None,
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: Some(flake8_quotes::settings::Options {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
@@ -354,6 +378,7 @@ mod tests {
per_file_ignores: None,
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
pep8_naming: None,
});
@@ -384,6 +409,7 @@ mod tests {
per_file_ignores: None,
dummy_variable_rgx: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: Some(flake8_quotes::settings::Options {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,

View File

@@ -37,6 +37,15 @@ pub fn parse_strings(value: &str) -> Vec<String> {
.collect()
}
/// Parse a boolean.
pub fn parse_bool(value: &str) -> Result<bool> {
match value.trim() {
"true" => Ok(true),
"false" => Ok(false),
_ => Err(anyhow::anyhow!("Unexpected boolean value: {value}")),
}
}
#[derive(Debug)]
struct Token {
token_name: TokenType,

View File

@@ -12,6 +12,7 @@ pub enum Plugin {
Flake8Docstrings,
Flake8Print,
Flake8Quotes,
Flake8Annotations,
PEP8Naming,
Pyupgrade,
}
@@ -27,6 +28,7 @@ impl FromStr for Plugin {
"flake8-docstrings" => Ok(Plugin::Flake8Docstrings),
"flake8-print" => Ok(Plugin::Flake8Print),
"flake8-quotes" => Ok(Plugin::Flake8Quotes),
"flake8-annotations" => Ok(Plugin::Flake8Annotations),
"pep8-naming" => Ok(Plugin::PEP8Naming),
"pyupgrade" => Ok(Plugin::Pyupgrade),
_ => Err(anyhow!("Unknown plugin: {}", string)),
@@ -58,6 +60,7 @@ impl Plugin {
}
Plugin::Flake8Print => vec![CheckCodePrefix::T],
Plugin::Flake8Quotes => vec![CheckCodePrefix::Q],
Plugin::Flake8Annotations => vec![CheckCodePrefix::ANN],
Plugin::PEP8Naming => vec![CheckCodePrefix::N],
Plugin::Pyupgrade => vec![CheckCodePrefix::U],
}
@@ -262,6 +265,31 @@ pub fn infer_plugins(flake8: &HashMap<String, Option<String>>) -> Vec<Plugin> {
"avoid-escape" | "avoid_escape" => {
plugins.insert(Plugin::Flake8Quotes);
}
// flake8-annotations
"suppress-none-returning" | "suppress_none_returning" => {
plugins.insert(Plugin::Flake8Annotations);
}
"suppress-dummy-args" | "suppress_dummy_args" => {
plugins.insert(Plugin::Flake8Annotations);
}
"allow-untyped-defs" | "allow_untyped_defs" => {
plugins.insert(Plugin::Flake8Annotations);
}
"allow-untyped-nested" | "allow_untyped_nested" => {
plugins.insert(Plugin::Flake8Annotations);
}
"mypy-init-return" | "mypy_init_return" => {
plugins.insert(Plugin::Flake8Annotations);
}
"dispatch-decorators" | "dispatch_decorators" => {
plugins.insert(Plugin::Flake8Annotations);
}
"overload-decorators" | "overload_decorators" => {
plugins.insert(Plugin::Flake8Annotations);
}
"allow-star-arg-any" | "allow_star_arg_any" => {
plugins.insert(Plugin::Flake8Annotations);
}
// pep8-naming
"ignore-names" | "ignore_names" => {
plugins.insert(Plugin::PEP8Naming);

View File

@@ -22,3 +22,6 @@ data = [x for x in [1, 2, 3] if x in (1, 2)]
class TestClass:
1 == 1
print(1 == 1)

View File

@@ -3,3 +3,6 @@ if x is "abc":
if 123 is not y:
pass
if "123" is x < 3:
pass

View File

@@ -1,7 +1,7 @@
def func(a, A):
return a, A
def func(_, a, A):
return _, a, A
class Class:
def method(self, a, A):
return a, A
def method(self, _, a, A):
return _, a, A

View File

@@ -1,19 +1,43 @@
from abc import ABCMeta
class Class:
@classmethod
def bad_class_method(this):
def bad_method(this):
pass
if False:
def extra_bad_method(this):
pass
def good_method(self):
pass
@classmethod
def good_class_method(cls):
pass
def method(self):
def class_method(cls):
pass
@staticmethod
def static_method(x):
return x
def __init__(self):
...
def __new__(cls, *args, **kwargs):
...
def __init_subclass__(self, default_name, **kwargs):
...
class MetaClass(ABCMeta):
def bad_method(self):
pass
def good_method(cls):
pass
def func(x):
return x

View File

@@ -1,11 +1,11 @@
import random
from abc import ABCMeta
class Class:
def bad_method(this):
pass
if random.random(0, 2) == 0:
if False:
def extra_bad_method(this):
pass
@@ -21,6 +21,23 @@ class Class:
def static_method(x):
return x
def __init__(self):
...
def __new__(cls, *args, **kwargs):
...
def __init_subclass__(self, default_name, **kwargs):
...
class MetaClass(ABCMeta):
def bad_method(self):
pass
def good_method(cls):
pass
def func(x):
return x

3
resources/test/fixtures/U009_0.py vendored Normal file
View File

@@ -0,0 +1,3 @@
# coding=utf8
print('Hello world')

4
resources/test/fixtures/U009_1.py vendored Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
print('Hello world')

5
resources/test/fixtures/U009_2.py vendored Normal file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/python
# A coding comment is only valid in the first two lines, so this one is not checked.
# -*- coding: utf-8 -*-
print('Hello world')

4
resources/test/fixtures/U009_3.py vendored Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/python
# -*- coding: something-else -*-
print('Hello world')

View File

@@ -0,0 +1,33 @@
# Error
def foo(a, b):
pass
# Error
def foo(a: int, b):
pass
# Error
def foo(a: int, b) -> int:
pass
# Error
def foo(a: int, b: int):
pass
# Error
def foo():
pass
# OK
def foo(a: int, b: int) -> int:
pass
# OK
def foo() -> int:
pass

View File

@@ -0,0 +1,41 @@
"""Test case expected to be run with `mypy_init_return = True`."""
# Error
class Foo:
def __init__(self):
...
# Error
class Foo:
def __init__(self, foo):
...
# OK
class Foo:
def __init__(self, foo) -> None:
...
# OK
class Foo:
def __init__(self) -> None:
...
# OK
class Foo:
def __init__(self, foo: int):
...
# OK
class Foo:
def __init__(self, foo: int) -> None:
...
# Error
def __init__(self, foo: int):
...

View File

@@ -0,0 +1,26 @@
"""Test case expected to be run with `suppress_dummy_args = True`."""
# OK
def foo(_) -> None:
...
# OK
def foo(*_) -> None:
...
# OK
def foo(**_) -> None:
...
# OK
def foo(a: int, _) -> None:
...
# OK
def foo() -> None:
def bar(_) -> None:
...

View File

@@ -0,0 +1,55 @@
"""Test case expected to be run with `suppress_none_returning = True`."""
# OK
def foo():
a = 2 + 2
# OK
def foo():
return
# OK
def foo():
return None
# OK
def foo():
a = 2 + 2
if a == 4:
return
else:
return
# OK
def foo():
a = 2 + 2
if a == 4:
return None
else:
return
# OK
def foo():
def bar() -> bool:
return True
bar()
# Error
def foo():
return True
# Error
def foo():
a = 2 + 2
if a == 4:
return True
else:
return

View File

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

View File

@@ -11,7 +11,7 @@ use itertools::Itertools;
use ruff::checks::CheckCode;
use strum::IntoEnumIterator;
const FILE: &str = "../src/checks_gen.rs";
const FILE: &str = "src/checks_gen.rs";
#[derive(Parser)]
#[command(author, version, about, long_about = None)]

View File

@@ -3,13 +3,13 @@
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use ruff::checks::{CheckCategory, CheckCode};
use strum::IntoEnumIterator;
const FILE: &str = "../README.md";
const BEGIN_PRAGMA: &str = "<!-- Begin auto-generated sections. -->";
const END_PRAGMA: &str = "<!-- End auto-generated sections. -->";
@@ -64,7 +64,11 @@ pub fn main(cli: &Cli) -> Result<()> {
print!("{}", output);
} else {
// Read the existing file.
let existing = fs::read_to_string(FILE)?;
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("README.md");
let existing = fs::read_to_string(&file)?;
// Extract the prefix.
let index = existing
@@ -79,7 +83,7 @@ pub fn main(cli: &Cli) -> Result<()> {
let suffix = &existing[index..];
// Write the prefix, new contents, and suffix.
let mut f = OpenOptions::new().write(true).truncate(true).open(FILE)?;
let mut f = OpenOptions::new().write(true).truncate(true).open(&file)?;
write!(f, "{}\n\n", prefix)?;
write!(f, "{}", output)?;
write!(f, "{}", suffix)?;

View File

@@ -1,6 +1,7 @@
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use rustpython_ast::{Expr, Keyword};
use rustpython_parser::ast::{Located, Location};
fn id() -> usize {
@@ -30,9 +31,17 @@ pub struct FunctionScope {
pub uses_locals: bool,
}
#[derive(Clone, Debug, Default)]
pub struct ClassScope<'a> {
pub name: &'a str,
pub bases: &'a [Expr],
pub keywords: &'a [Keyword],
pub decorator_list: &'a [Expr],
}
#[derive(Clone, Debug)]
pub enum ScopeKind {
Class,
pub enum ScopeKind<'a> {
Class(ClassScope<'a>),
Function(FunctionScope),
Generator,
Module,
@@ -40,15 +49,15 @@ pub enum ScopeKind {
}
#[derive(Clone, Debug)]
pub struct Scope {
pub struct Scope<'a> {
pub id: usize,
pub kind: ScopeKind,
pub kind: ScopeKind<'a>,
pub import_starred: bool,
pub values: BTreeMap<String, Binding>,
}
impl Scope {
pub fn new(kind: ScopeKind) -> Self {
impl<'a> Scope<'a> {
pub fn new(kind: ScopeKind<'a>) -> Self {
Scope {
id: id(),
kind,
@@ -91,10 +100,6 @@ pub struct Binding {
pub used: Option<(usize, Range)>,
}
pub trait CheckLocator {
fn locate_check(&self, default: Range) -> Range;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum ImportKind {
Import,

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@
use std::collections::BTreeMap;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
@@ -11,6 +13,10 @@ use crate::noqa;
use crate::noqa::Directive;
use crate::settings::Settings;
// Regex from PEP263
static CODING_COMMENT_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[ \t\f]*#.*?coding[:=][ \t]*utf-?8").expect("Invalid regex"));
/// Whether the given line is too long and should be reported.
fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
if length > limit {
@@ -52,6 +58,27 @@ pub fn check_lines(
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
if lineno < 2 {
// PEP3120 makes utf-8 the default encoding.
if CODING_COMMENT_REGEX.is_match(line) {
let line_length = line.len();
let mut check = Check::new(
CheckKind::PEP3120UnnecessaryCodingComment,
Range {
location: Location::new(lineno + 1, 0),
end_location: Location::new(lineno + 1, line_length + 1),
},
);
if autofix.patch() {
check.amend(Fix::deletion(
Location::new(lineno + 1, 0),
Location::new(lineno + 1, line_length + 1),
));
}
line_checks.push(check);
}
}
if enforce_noqa {
noqa_directives
.entry(noqa_lineno)

View File

@@ -115,6 +115,17 @@ pub enum CheckCode {
Q001,
Q002,
Q003,
// flake8-annotations
ANN001,
ANN002,
ANN003,
ANN101,
ANN102,
ANN201,
ANN202,
ANN204,
ANN205,
ANN206,
// pyupgrade
U001,
U002,
@@ -124,6 +135,7 @@ pub enum CheckCode {
U006,
U007,
U008,
U009,
// pydocstyle
D100,
D101,
@@ -204,6 +216,7 @@ pub enum CheckCategory {
Flake8Builtins,
Flake8Print,
Flake8Quotes,
Flake8Annotations,
Ruff,
Meta,
}
@@ -218,6 +231,7 @@ impl CheckCategory {
CheckCategory::Flake8Comprehensions => "flake8-comprehensions",
CheckCategory::Flake8Print => "flake8-print",
CheckCategory::Flake8Quotes => "flake8-quotes",
CheckCategory::Flake8Annotations => "flake8-annotations",
CheckCategory::Pyupgrade => "pyupgrade",
CheckCategory::Pydocstyle => "pydocstyle",
CheckCategory::PEP8Naming => "pep8-naming",
@@ -241,6 +255,9 @@ impl CheckCategory {
}
CheckCategory::Flake8Print => Some("https://pypi.org/project/flake8-print/5.0.0/"),
CheckCategory::Flake8Quotes => Some("https://pypi.org/project/flake8-quotes/3.3.1/"),
CheckCategory::Flake8Annotations => {
Some("https://pypi.org/project/flake8-annotations/2.9.1/")
}
CheckCategory::Pyupgrade => Some("https://pypi.org/project/pyupgrade/3.2.0/"),
CheckCategory::Pydocstyle => Some("https://pypi.org/project/pydocstyle/6.1.1/"),
CheckCategory::PEP8Naming => Some("https://pypi.org/project/pep8-naming/0.13.2/"),
@@ -356,6 +373,17 @@ pub enum CheckKind {
BadQuotesMultilineString(Quote),
BadQuotesDocstring(Quote),
AvoidQuoteEscape,
// flake8-annotations
MissingTypeFunctionArgument,
MissingTypeArgs,
MissingTypeKwargs,
MissingTypeSelf,
MissingTypeCls,
MissingReturnTypePublicFunction,
MissingReturnTypePrivateFunction,
MissingReturnTypeMagicMethod,
MissingReturnTypeStaticMethod,
MissingReturnTypeClassMethod,
// pyupgrade
TypeOfPrimitive(Primitive),
UnnecessaryAbspath,
@@ -365,6 +393,7 @@ pub enum CheckKind {
UsePEP585Annotation(String),
UsePEP604Annotation,
SuperCallWithParameters,
PEP3120UnnecessaryCodingComment,
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
@@ -438,7 +467,9 @@ impl CheckCode {
/// physical lines).
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::E501 | CheckCode::W292 | CheckCode::M001 => &LintSource::Lines,
CheckCode::E501 | CheckCode::W292 | CheckCode::M001 | CheckCode::U009 => {
&LintSource::Lines
}
CheckCode::Q000
| CheckCode::Q001
| CheckCode::Q002
@@ -561,6 +592,17 @@ impl CheckCode {
CheckCode::Q001 => CheckKind::BadQuotesMultilineString(Quote::Double),
CheckCode::Q002 => CheckKind::BadQuotesDocstring(Quote::Double),
CheckCode::Q003 => CheckKind::AvoidQuoteEscape,
// flake8-annotations
CheckCode::ANN001 => CheckKind::MissingTypeFunctionArgument,
CheckCode::ANN002 => CheckKind::MissingTypeArgs,
CheckCode::ANN003 => CheckKind::MissingTypeKwargs,
CheckCode::ANN101 => CheckKind::MissingTypeSelf,
CheckCode::ANN102 => CheckKind::MissingTypeCls,
CheckCode::ANN201 => CheckKind::MissingReturnTypePublicFunction,
CheckCode::ANN202 => CheckKind::MissingReturnTypePrivateFunction,
CheckCode::ANN204 => CheckKind::MissingReturnTypeMagicMethod,
CheckCode::ANN205 => CheckKind::MissingReturnTypeStaticMethod,
CheckCode::ANN206 => CheckKind::MissingReturnTypeClassMethod,
// pyupgrade
CheckCode::U001 => CheckKind::UselessMetaclassType,
CheckCode::U002 => CheckKind::UnnecessaryAbspath,
@@ -573,6 +615,7 @@ impl CheckCode {
CheckCode::U006 => CheckKind::UsePEP585Annotation("List".to_string()),
CheckCode::U007 => CheckKind::UsePEP604Annotation,
CheckCode::U008 => CheckKind::SuperCallWithParameters,
CheckCode::U009 => CheckKind::PEP3120UnnecessaryCodingComment,
// pydocstyle
CheckCode::D100 => CheckKind::PublicModule,
CheckCode::D101 => CheckKind::PublicClass,
@@ -742,6 +785,16 @@ impl CheckCode {
CheckCode::Q001 => CheckCategory::Flake8Quotes,
CheckCode::Q002 => CheckCategory::Flake8Quotes,
CheckCode::Q003 => CheckCategory::Flake8Quotes,
CheckCode::ANN001 => CheckCategory::Flake8Annotations,
CheckCode::ANN002 => CheckCategory::Flake8Annotations,
CheckCode::ANN003 => CheckCategory::Flake8Annotations,
CheckCode::ANN101 => CheckCategory::Flake8Annotations,
CheckCode::ANN102 => CheckCategory::Flake8Annotations,
CheckCode::ANN201 => CheckCategory::Flake8Annotations,
CheckCode::ANN202 => CheckCategory::Flake8Annotations,
CheckCode::ANN204 => CheckCategory::Flake8Annotations,
CheckCode::ANN205 => CheckCategory::Flake8Annotations,
CheckCode::ANN206 => CheckCategory::Flake8Annotations,
CheckCode::U001 => CheckCategory::Pyupgrade,
CheckCode::U002 => CheckCategory::Pyupgrade,
CheckCode::U003 => CheckCategory::Pyupgrade,
@@ -750,6 +803,7 @@ impl CheckCode {
CheckCode::U006 => CheckCategory::Pyupgrade,
CheckCode::U007 => CheckCategory::Pyupgrade,
CheckCode::U008 => CheckCategory::Pyupgrade,
CheckCode::U009 => CheckCategory::Pyupgrade,
CheckCode::D100 => CheckCategory::Pydocstyle,
CheckCode::D101 => CheckCategory::Pydocstyle,
CheckCode::D102 => CheckCategory::Pydocstyle,
@@ -909,6 +963,17 @@ impl CheckKind {
CheckKind::BadQuotesMultilineString(_) => &CheckCode::Q001,
CheckKind::BadQuotesDocstring(_) => &CheckCode::Q002,
CheckKind::AvoidQuoteEscape => &CheckCode::Q003,
// flake8-annotations
CheckKind::MissingTypeFunctionArgument => &CheckCode::ANN001,
CheckKind::MissingTypeArgs => &CheckCode::ANN002,
CheckKind::MissingTypeKwargs => &CheckCode::ANN003,
CheckKind::MissingTypeSelf => &CheckCode::ANN101,
CheckKind::MissingTypeCls => &CheckCode::ANN102,
CheckKind::MissingReturnTypePublicFunction => &CheckCode::ANN201,
CheckKind::MissingReturnTypePrivateFunction => &CheckCode::ANN202,
CheckKind::MissingReturnTypeMagicMethod => &CheckCode::ANN204,
CheckKind::MissingReturnTypeStaticMethod => &CheckCode::ANN205,
CheckKind::MissingReturnTypeClassMethod => &CheckCode::ANN206,
// pyupgrade
CheckKind::TypeOfPrimitive(_) => &CheckCode::U003,
CheckKind::UnnecessaryAbspath => &CheckCode::U002,
@@ -918,6 +983,7 @@ impl CheckKind {
CheckKind::UsePEP604Annotation => &CheckCode::U007,
CheckKind::UselessObjectInheritance(_) => &CheckCode::U004,
CheckKind::SuperCallWithParameters => &CheckCode::U008,
CheckKind::PEP3120UnnecessaryCodingComment => &CheckCode::U009,
// pydocstyle
CheckKind::BlankLineAfterLastSection(_) => &CheckCode::D413,
CheckKind::BlankLineAfterSection(_) => &CheckCode::D410,
@@ -1293,6 +1359,33 @@ impl CheckKind {
CheckKind::AvoidQuoteEscape => {
"Change outer quotes to avoid escaping inner quotes".to_string()
}
// flake8-annotations
CheckKind::MissingTypeFunctionArgument => {
"Missing type annotation for function argument".to_string()
}
CheckKind::MissingTypeArgs => "Missing type annotation for `*args`".to_string(),
CheckKind::MissingTypeKwargs => "Missing type annotation for `**kwargs`".to_string(),
CheckKind::MissingTypeSelf => {
"Missing type annotation for `self` in method".to_string()
}
CheckKind::MissingTypeCls => {
"Missing type annotation for `cls` in classmethod".to_string()
}
CheckKind::MissingReturnTypePublicFunction => {
"Missing return type annotation for public function".to_string()
}
CheckKind::MissingReturnTypePrivateFunction => {
"Missing return type annotation for private function".to_string()
}
CheckKind::MissingReturnTypeMagicMethod => {
"Missing return type annotation for magic method".to_string()
}
CheckKind::MissingReturnTypeStaticMethod => {
"Missing return type annotation for staticmethod".to_string()
}
CheckKind::MissingReturnTypeClassMethod => {
"Missing return type annotation for classmethod".to_string()
}
// pyupgrade
CheckKind::TypeOfPrimitive(primitive) => {
format!("Use `{}` instead of `type(...)`", primitive.builtin())
@@ -1478,6 +1571,9 @@ impl CheckKind {
CheckKind::ErrorSuffixOnExceptionName(name) => {
format!("Exception name `{name}` should be named with an Error suffix")
}
CheckKind::PEP3120UnnecessaryCodingComment => {
"utf-8 encoding declaration is unnecessary".to_string()
}
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => {
format!(
@@ -1583,6 +1679,8 @@ impl CheckKind {
| CheckKind::UsePEP604Annotation
| CheckKind::UselessMetaclassType
| CheckKind::UselessObjectInheritance(_)
| CheckKind::PEP3120UnnecessaryCodingComment
| CheckKind::IsLiteral
)
}
}

View File

@@ -13,6 +13,23 @@ pub enum CheckCodePrefix {
A001,
A002,
A003,
ANN,
ANN0,
ANN00,
ANN001,
ANN002,
ANN003,
ANN1,
ANN10,
ANN101,
ANN102,
ANN2,
ANN20,
ANN201,
ANN202,
ANN204,
ANN205,
ANN206,
B,
B0,
B00,
@@ -230,6 +247,7 @@ pub enum CheckCodePrefix {
U006,
U007,
U008,
U009,
W,
W2,
W29,
@@ -256,6 +274,46 @@ impl CheckCodePrefix {
CheckCodePrefix::A001 => vec![CheckCode::A001],
CheckCodePrefix::A002 => vec![CheckCode::A002],
CheckCodePrefix::A003 => vec![CheckCode::A003],
CheckCodePrefix::ANN => vec![
CheckCode::ANN001,
CheckCode::ANN002,
CheckCode::ANN003,
CheckCode::ANN101,
CheckCode::ANN102,
CheckCode::ANN201,
CheckCode::ANN202,
CheckCode::ANN204,
CheckCode::ANN205,
CheckCode::ANN206,
],
CheckCodePrefix::ANN0 => vec![CheckCode::ANN001, CheckCode::ANN002, CheckCode::ANN003],
CheckCodePrefix::ANN00 => vec![CheckCode::ANN001, CheckCode::ANN002, CheckCode::ANN003],
CheckCodePrefix::ANN001 => vec![CheckCode::ANN001],
CheckCodePrefix::ANN002 => vec![CheckCode::ANN002],
CheckCodePrefix::ANN003 => vec![CheckCode::ANN003],
CheckCodePrefix::ANN1 => vec![CheckCode::ANN101, CheckCode::ANN102],
CheckCodePrefix::ANN10 => vec![CheckCode::ANN101, CheckCode::ANN102],
CheckCodePrefix::ANN101 => vec![CheckCode::ANN101],
CheckCodePrefix::ANN102 => vec![CheckCode::ANN102],
CheckCodePrefix::ANN2 => vec![
CheckCode::ANN201,
CheckCode::ANN202,
CheckCode::ANN204,
CheckCode::ANN205,
CheckCode::ANN206,
],
CheckCodePrefix::ANN20 => vec![
CheckCode::ANN201,
CheckCode::ANN202,
CheckCode::ANN204,
CheckCode::ANN205,
CheckCode::ANN206,
],
CheckCodePrefix::ANN201 => vec![CheckCode::ANN201],
CheckCodePrefix::ANN202 => vec![CheckCode::ANN202],
CheckCodePrefix::ANN204 => vec![CheckCode::ANN204],
CheckCodePrefix::ANN205 => vec![CheckCode::ANN205],
CheckCodePrefix::ANN206 => vec![CheckCode::ANN206],
CheckCodePrefix::B => vec![
CheckCode::B002,
CheckCode::B003,
@@ -877,6 +935,7 @@ impl CheckCodePrefix {
CheckCode::U006,
CheckCode::U007,
CheckCode::U008,
CheckCode::U009,
],
CheckCodePrefix::U0 => vec![
CheckCode::U001,
@@ -887,6 +946,7 @@ impl CheckCodePrefix {
CheckCode::U006,
CheckCode::U007,
CheckCode::U008,
CheckCode::U009,
],
CheckCodePrefix::U00 => vec![
CheckCode::U001,
@@ -897,6 +957,7 @@ impl CheckCodePrefix {
CheckCode::U006,
CheckCode::U007,
CheckCode::U008,
CheckCode::U009,
],
CheckCodePrefix::U001 => vec![CheckCode::U001],
CheckCodePrefix::U002 => vec![CheckCode::U002],
@@ -906,6 +967,7 @@ impl CheckCodePrefix {
CheckCodePrefix::U006 => vec![CheckCode::U006],
CheckCodePrefix::U007 => vec![CheckCode::U007],
CheckCodePrefix::U008 => vec![CheckCode::U008],
CheckCodePrefix::U009 => vec![CheckCode::U009],
CheckCodePrefix::W => vec![CheckCode::W292, CheckCode::W605],
CheckCodePrefix::W2 => vec![CheckCode::W292],
CheckCodePrefix::W29 => vec![CheckCode::W292],
@@ -926,6 +988,23 @@ impl CheckCodePrefix {
CheckCodePrefix::A001 => PrefixSpecificity::Explicit,
CheckCodePrefix::A002 => PrefixSpecificity::Explicit,
CheckCodePrefix::A003 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN => PrefixSpecificity::Category,
CheckCodePrefix::ANN0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::ANN00 => PrefixSpecificity::Tens,
CheckCodePrefix::ANN001 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN002 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN003 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN1 => PrefixSpecificity::Hundreds,
CheckCodePrefix::ANN10 => PrefixSpecificity::Tens,
CheckCodePrefix::ANN101 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN102 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN2 => PrefixSpecificity::Hundreds,
CheckCodePrefix::ANN20 => PrefixSpecificity::Tens,
CheckCodePrefix::ANN201 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN202 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN204 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN205 => PrefixSpecificity::Explicit,
CheckCodePrefix::ANN206 => PrefixSpecificity::Explicit,
CheckCodePrefix::B => PrefixSpecificity::Category,
CheckCodePrefix::B0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::B00 => PrefixSpecificity::Tens,
@@ -1143,6 +1222,7 @@ impl CheckCodePrefix {
CheckCodePrefix::U006 => PrefixSpecificity::Explicit,
CheckCodePrefix::U007 => PrefixSpecificity::Explicit,
CheckCodePrefix::U008 => PrefixSpecificity::Explicit,
CheckCodePrefix::U009 => PrefixSpecificity::Explicit,
CheckCodePrefix::W => PrefixSpecificity::Category,
CheckCodePrefix::W2 => PrefixSpecificity::Hundreds,
CheckCodePrefix::W29 => PrefixSpecificity::Tens,

View File

@@ -10,7 +10,7 @@ use crate::checks_gen::CheckCodePrefix;
use crate::logging::LogLevel;
use crate::printer::SerializationFormat;
use crate::settings::configuration::Configuration;
use crate::settings::types::{PatternPrefixPair, PythonVersion};
use crate::settings::types::{PatternPrefixPair, PerFileIgnore, PythonVersion};
#[derive(Debug, Parser)]
#[command(author, about = "ruff: An extremely fast Python linter.")]
@@ -168,7 +168,8 @@ pub fn warn_on(
/// Collect a list of `PatternPrefixPair` structs as a `BTreeMap`.
pub fn collect_per_file_ignores(
pairs: Vec<PatternPrefixPair>,
) -> BTreeMap<String, Vec<CheckCodePrefix>> {
project_root: &Option<PathBuf>,
) -> Vec<PerFileIgnore> {
let mut per_file_ignores: BTreeMap<String, Vec<CheckCodePrefix>> = BTreeMap::new();
for pair in pairs {
per_file_ignores
@@ -177,4 +178,7 @@ pub fn collect_per_file_ignores(
.push(pair.prefix);
}
per_file_ignores
.iter()
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, prefixes, project_root))
.collect()
}

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use libcst_native::Module;
use libcst_native::{Expr, Module, SmallStatement, Statement};
pub fn match_module(module_text: &str) -> Result<Module> {
match libcst_native::parse_module(module_text, None) {
@@ -7,3 +7,15 @@ pub fn match_module(module_text: &str) -> Result<Module> {
Err(_) => Err(anyhow::anyhow!("Failed to extract CST from source.")),
}
}
pub fn match_expr<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Expr<'b>> {
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
if let Some(SmallStatement::Expr(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
Err(anyhow::anyhow!("Expected node to be: SmallStatement::Expr"))
}
} else {
Err(anyhow::anyhow!("Expected node to be: Statement::Simple"))
}
}

View File

@@ -0,0 +1,131 @@
pub mod plugins;
pub mod settings;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use rustpython_parser::lexer::LexResult;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::linter::tokenize;
use crate::{flake8_annotations, fs, linter, noqa, Settings, SourceCodeLocator};
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = tokenize(&contents);
let locator = SourceCodeLocator::new(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
linter::check_path(
path,
&contents,
tokens,
&locator,
&noqa_line_for,
settings,
autofix,
)
}
#[test]
fn defaults() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_annotations/annotation_presence.py"),
&Settings {
..Settings::for_rules(vec![
CheckCode::ANN001,
CheckCode::ANN002,
CheckCode::ANN003,
CheckCode::ANN101,
CheckCode::ANN102,
CheckCode::ANN201,
CheckCode::ANN202,
CheckCode::ANN204,
CheckCode::ANN205,
CheckCode::ANN206,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn suppress_dummy_args() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_annotations/suppress_dummy_args.py"),
&Settings {
flake8_annotations: flake8_annotations::settings::Settings {
mypy_init_return: false,
suppress_dummy_args: true,
suppress_none_returning: false,
},
..Settings::for_rules(vec![
CheckCode::ANN001,
CheckCode::ANN002,
CheckCode::ANN003,
CheckCode::ANN101,
CheckCode::ANN102,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn mypy_init_return() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_annotations/mypy_init_return.py"),
&Settings {
flake8_annotations: flake8_annotations::settings::Settings {
mypy_init_return: true,
suppress_dummy_args: false,
suppress_none_returning: false,
},
..Settings::for_rules(vec![
CheckCode::ANN201,
CheckCode::ANN202,
CheckCode::ANN204,
CheckCode::ANN205,
CheckCode::ANN206,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn suppress_none_returning() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_annotations/suppress_none_returning.py"),
&Settings {
flake8_annotations: flake8_annotations::settings::Settings {
mypy_init_return: false,
suppress_dummy_args: false,
suppress_none_returning: true,
},
..Settings::for_rules(vec![
CheckCode::ANN201,
CheckCode::ANN202,
CheckCode::ANN204,
CheckCode::ANN205,
CheckCode::ANN206,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@@ -0,0 +1,326 @@
use rustpython_ast::{Arguments, Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::check_ast::Checker;
use crate::checks::{CheckCode, CheckKind};
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::visibility::Visibility;
use crate::{visibility, Check};
#[derive(Default)]
struct ReturnStatementVisitor<'a> {
returns: Vec<&'a Option<Box<Expr>>>,
}
impl<'a, 'b> Visitor<'b> for ReturnStatementVisitor<'a>
where
'b: 'a,
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
// No recurse.
}
StmtKind::Return { value } => self.returns.push(value),
_ => visitor::walk_stmt(self, stmt),
}
}
}
fn is_none_returning(stmt: &Stmt) -> bool {
let mut visitor: ReturnStatementVisitor = Default::default();
for stmt in match_body(stmt) {
visitor.visit_stmt(stmt);
}
for expr in visitor.returns.into_iter().flatten() {
if !matches!(
expr.node,
ExprKind::Constant {
value: Constant::None,
..
}
) {
return false;
}
}
true
}
fn match_args(stmt: &Stmt) -> &Arguments {
match &stmt.node {
StmtKind::FunctionDef { args, .. } | StmtKind::AsyncFunctionDef { args, .. } => args,
_ => panic!("Found non-FunctionDef in match_args"),
}
}
fn match_body(stmt: &Stmt) -> &Vec<Stmt> {
match &stmt.node {
StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => body,
_ => panic!("Found non-FunctionDef in match_body"),
}
}
fn match_returns(stmt: &Stmt) -> &Option<Box<Expr>> {
match &stmt.node {
StmtKind::FunctionDef { returns, .. } | StmtKind::AsyncFunctionDef { returns, .. } => {
returns
}
_ => panic!("Found non-FunctionDef in match_returns"),
}
}
/// Generate flake8-annotation checks for a given `Definition`.
pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &Visibility) {
match &definition.kind {
DefinitionKind::Module => {}
DefinitionKind::Package => {}
DefinitionKind::Class(_) => {}
DefinitionKind::NestedClass(_) => {}
DefinitionKind::Function(stmt) | DefinitionKind::NestedFunction(stmt) => {
let args = match_args(stmt);
let returns = match_returns(stmt);
// ANN001
for arg in args
.args
.iter()
.chain(args.posonlyargs.iter())
.chain(args.kwonlyargs.iter())
{
if arg.node.annotation.is_none() {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.enabled.contains(&CheckCode::ANN001) {
checker.add_check(Check::new(
CheckKind::MissingTypeFunctionArgument,
Range::from_located(arg),
));
}
}
}
}
// ANN002
if let Some(arg) = &args.vararg {
if arg.node.annotation.is_none() {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.enabled.contains(&CheckCode::ANN002) {
checker.add_check(Check::new(
CheckKind::MissingTypeArgs,
Range::from_located(arg),
));
}
}
}
}
// ANN003
if let Some(arg) = &args.kwarg {
if arg.node.annotation.is_none() {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.enabled.contains(&CheckCode::ANN003) {
checker.add_check(Check::new(
CheckKind::MissingTypeKwargs,
Range::from_located(arg),
));
}
}
}
}
// ANN201, ANN202
if returns.is_none() {
// Allow omission of return annotation in `__init__` functions, if the function
// only returns `None` (explicitly or implicitly).
if checker.settings.flake8_annotations.suppress_none_returning
&& is_none_returning(stmt)
{
return;
}
match visibility {
Visibility::Public => {
if checker.settings.enabled.contains(&CheckCode::ANN201) {
checker.add_check(Check::new(
CheckKind::MissingReturnTypePublicFunction,
Range::from_located(stmt),
));
}
}
Visibility::Private => {
if checker.settings.enabled.contains(&CheckCode::ANN202) {
checker.add_check(Check::new(
CheckKind::MissingReturnTypePrivateFunction,
Range::from_located(stmt),
));
}
}
}
}
}
DefinitionKind::Method(stmt) => {
let args = match_args(stmt);
let returns = match_returns(stmt);
let mut has_any_typed_arg = false;
// ANN001
for arg in args
.args
.iter()
.chain(args.posonlyargs.iter())
.chain(args.kwonlyargs.iter())
.skip(
// If this is a non-static method, skip `cls` or `self`.
usize::from(!visibility::is_staticmethod(stmt)),
)
{
if arg.node.annotation.is_none() {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.enabled.contains(&CheckCode::ANN001) {
checker.add_check(Check::new(
CheckKind::MissingTypeFunctionArgument,
Range::from_located(arg),
));
}
}
} else {
has_any_typed_arg = true;
}
}
// ANN002
if let Some(arg) = &args.vararg {
if arg.node.annotation.is_none() {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.enabled.contains(&CheckCode::ANN002) {
checker.add_check(Check::new(
CheckKind::MissingTypeArgs,
Range::from_located(arg),
));
}
}
} else {
has_any_typed_arg = true;
}
}
// ANN003
if let Some(arg) = &args.kwarg {
if arg.node.annotation.is_none() {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.enabled.contains(&CheckCode::ANN003) {
checker.add_check(Check::new(
CheckKind::MissingTypeKwargs,
Range::from_located(arg),
));
}
}
} else {
has_any_typed_arg = true;
}
}
// ANN101, ANN102
if !visibility::is_staticmethod(stmt) {
if let Some(arg) = args.args.first() {
if arg.node.annotation.is_none() {
if visibility::is_classmethod(stmt) {
if checker.settings.enabled.contains(&CheckCode::ANN101) {
checker.add_check(Check::new(
CheckKind::MissingTypeCls,
Range::from_located(arg),
));
}
} else {
if checker.settings.enabled.contains(&CheckCode::ANN102) {
checker.add_check(Check::new(
CheckKind::MissingTypeSelf,
Range::from_located(arg),
));
}
}
}
}
}
// ANN201, ANN202
if returns.is_none() {
// Allow omission of return annotation in `__init__` functions, if the function
// only returns `None` (explicitly or implicitly).
if checker.settings.flake8_annotations.suppress_none_returning
&& is_none_returning(stmt)
{
return;
}
if visibility::is_classmethod(stmt) {
if checker.settings.enabled.contains(&CheckCode::ANN206) {
checker.add_check(Check::new(
CheckKind::MissingReturnTypeClassMethod,
Range::from_located(stmt),
));
}
} else if visibility::is_staticmethod(stmt) {
if checker.settings.enabled.contains(&CheckCode::ANN205) {
checker.add_check(Check::new(
CheckKind::MissingReturnTypeStaticMethod,
Range::from_located(stmt),
));
}
} else if visibility::is_magic(stmt) {
if checker.settings.enabled.contains(&CheckCode::ANN204) {
checker.add_check(Check::new(
CheckKind::MissingReturnTypeMagicMethod,
Range::from_located(stmt),
));
}
} else if visibility::is_init(stmt) {
// Allow omission of return annotation in `__init__` functions, as long as at
// least one argument is typed.
if checker.settings.enabled.contains(&CheckCode::ANN204) {
if !(checker.settings.flake8_annotations.mypy_init_return
&& has_any_typed_arg)
{
checker.add_check(Check::new(
CheckKind::MissingReturnTypeMagicMethod,
Range::from_located(stmt),
));
}
}
} else {
match visibility {
Visibility::Public => {
if checker.settings.enabled.contains(&CheckCode::ANN201) {
checker.add_check(Check::new(
CheckKind::MissingReturnTypePublicFunction,
Range::from_located(stmt),
));
}
}
Visibility::Private => {
if checker.settings.enabled.contains(&CheckCode::ANN202) {
checker.add_check(Check::new(
CheckKind::MissingReturnTypePrivateFunction,
Range::from_located(stmt),
));
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,36 @@
//! Settings for the `flake-annotations` plugin.
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
/// Allow omission of a return type hint for `__init__` if at least one
/// argument is annotated.
pub mypy_init_return: Option<bool>,
/// Suppress ANN000-level errors for dummy arguments, like `_`.
pub suppress_dummy_args: Option<bool>,
/// Suppress ANN200-level errors for functions that meet one of the
/// following criteria:
/// - Contain no `return` statement
/// - Explicit `return` statement(s) all return `None` (explicitly or
/// implicitly).
pub suppress_none_returning: Option<bool>,
}
#[derive(Debug, Hash, Default)]
pub struct Settings {
pub mypy_init_return: bool,
pub suppress_dummy_args: bool,
pub suppress_none_returning: bool,
}
impl Settings {
pub fn from_options(options: Options) -> Self {
Self {
mypy_init_return: options.mypy_init_return.unwrap_or_default(),
suppress_dummy_args: options.suppress_dummy_args.unwrap_or_default(),
suppress_none_returning: options.suppress_none_returning.unwrap_or_default(),
}
}
}

View File

@@ -0,0 +1,69 @@
---
source: src/flake8_annotations/mod.rs
expression: checks
---
- kind: MissingReturnTypePublicFunction
location:
row: 2
column: 0
end_location:
row: 7
column: 0
fix: ~
- kind: MissingTypeFunctionArgument
location:
row: 2
column: 8
end_location:
row: 2
column: 9
fix: ~
- kind: MissingTypeFunctionArgument
location:
row: 2
column: 11
end_location:
row: 2
column: 12
fix: ~
- kind: MissingReturnTypePublicFunction
location:
row: 7
column: 0
end_location:
row: 12
column: 0
fix: ~
- kind: MissingTypeFunctionArgument
location:
row: 7
column: 16
end_location:
row: 7
column: 17
fix: ~
- kind: MissingTypeFunctionArgument
location:
row: 12
column: 16
end_location:
row: 12
column: 17
fix: ~
- kind: MissingReturnTypePublicFunction
location:
row: 17
column: 0
end_location:
row: 22
column: 0
fix: ~
- kind: MissingReturnTypePublicFunction
location:
row: 22
column: 0
end_location:
row: 27
column: 0
fix: ~

View File

@@ -0,0 +1,29 @@
---
source: src/flake8_annotations/mod.rs
expression: checks
---
- kind: MissingReturnTypeMagicMethod
location:
row: 5
column: 4
end_location:
row: 10
column: 0
fix: ~
- kind: MissingReturnTypeMagicMethod
location:
row: 11
column: 4
end_location:
row: 16
column: 0
fix: ~
- kind: MissingReturnTypePrivateFunction
location:
row: 40
column: 0
end_location:
row: 42
column: 0
fix: ~

View File

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

View File

@@ -0,0 +1,21 @@
---
source: src/flake8_annotations/mod.rs
expression: checks
---
- kind: MissingReturnTypePublicFunction
location:
row: 45
column: 0
end_location:
row: 50
column: 0
fix: ~
- kind: MissingReturnTypePublicFunction
location:
row: 50
column: 0
end_location:
row: 56
column: 0
fix: ~

View File

@@ -1,6 +1,6 @@
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
@@ -42,10 +42,7 @@ pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: &Optio
..
} = &test.node
{
let mut check = Check::new(
CheckKind::DoNotAssertFalse,
checker.locate_check(Range::from_located(test)),
);
let mut check = Check::new(CheckKind::DoNotAssertFalse, Range::from_located(test));
if checker.patch() {
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_stmt(&assertion_error(msg)) {

View File

@@ -1,7 +1,7 @@
use rustpython_ast::{ExprKind, Stmt, Withitem};
use crate::ast::helpers::match_name_or_attr;
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
@@ -17,7 +17,7 @@ pub fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[With
{
checker.add_check(Check::new(
CheckKind::NoAssertRaisesException,
checker.locate_check(Range::from_located(stmt)),
Range::from_located(stmt),
));
}
}

View File

@@ -1,6 +1,6 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
@@ -14,7 +14,7 @@ pub fn assignment_to_os_environ(checker: &mut Checker, targets: &[Expr]) {
if id == "os" {
checker.add_check(Check::new(
CheckKind::AssignmentToOsEnviron,
checker.locate_check(Range::from_located(target)),
Range::from_located(target),
));
}
}

View File

@@ -1,6 +1,6 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
@@ -9,7 +9,7 @@ pub fn cannot_raise_literal(checker: &mut Checker, expr: &Expr) {
if let ExprKind::Constant { .. } = &expr.node {
checker.add_check(Check::new(
CheckKind::CannotRaiseLiteral,
checker.locate_check(Range::from_located(expr)),
Range::from_located(expr),
));
}
}

View File

@@ -4,7 +4,7 @@ use itertools::Itertools;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Stmt};
use crate::ast::helpers;
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
@@ -47,7 +47,7 @@ pub fn duplicate_handler_exceptions(
CheckKind::DuplicateHandlerException(
duplicates.into_iter().sorted().collect::<Vec<String>>(),
),
checker.locate_check(Range::from_located(expr)),
Range::from_located(expr),
);
if checker.patch() {
// TODO(charlie): If we have a single element, remove the tuple.
@@ -106,7 +106,7 @@ pub fn duplicate_exceptions(checker: &mut Checker, stmt: &Stmt, handlers: &[Exce
for duplicate in duplicates.into_iter().sorted() {
checker.add_check(Check::new(
CheckKind::DuplicateTryBlockException(duplicate),
checker.locate_check(Range::from_located(stmt)),
Range::from_located(stmt),
));
}
}

View File

@@ -1,7 +1,7 @@
use rustpython_ast::{Arguments, Constant, Expr, ExprKind};
use crate::ast::helpers::compose_call_path;
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::check_ast::Checker;
@@ -91,6 +91,6 @@ pub fn function_call_argument_default(checker: &mut Checker, arguments: &Argumen
visitor.visit_expr(expr);
}
for (check, range) in visitor.checks {
checker.add_check(Check::new(check, checker.locate_check(range)));
checker.add_check(Check::new(check, range));
}
}

View File

@@ -1,6 +1,6 @@
use rustpython_ast::{Arguments, Expr, ExprKind};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
@@ -45,14 +45,14 @@ pub fn mutable_argument_default(checker: &mut Checker, arguments: &Arguments) {
| ExprKind::SetComp { .. } => {
checker.add_check(Check::new(
CheckKind::MutableArgumentDefault,
checker.locate_check(Range::from_located(expr)),
Range::from_located(expr),
));
}
ExprKind::Call { func, .. } => {
if is_mutable_func(func) {
checker.add_check(Check::new(
CheckKind::MutableArgumentDefault,
checker.locate_check(Range::from_located(expr)),
Range::from_located(expr),
));
}
}

View File

@@ -1,6 +1,6 @@
use rustpython_ast::{Excepthandler, ExcepthandlerKind, ExprKind};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
@@ -13,7 +13,7 @@ pub fn redundant_tuple_in_exception_handler(checker: &mut Checker, handlers: &[E
if elts.len() == 1 {
checker.add_check(Check::new(
CheckKind::RedundantTupleInExceptionHandler(elts[0].to_string()),
checker.locate_check(Range::from_located(type_)),
Range::from_located(type_),
));
}
}

View File

@@ -1,6 +1,6 @@
use rustpython_ast::{Expr, ExprKind, Unaryop};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
@@ -11,7 +11,7 @@ pub fn unary_prefix_increment(checker: &mut Checker, expr: &Expr, op: &Unaryop,
if matches!(op, Unaryop::UAdd) {
checker.add_check(Check::new(
CheckKind::UnaryPrefixIncrement,
checker.locate_check(Range::from_located(expr)),
Range::from_located(expr),
))
}
}

View File

@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use rustpython_ast::{Expr, ExprKind, Stmt};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::autofix::Fix;
@@ -64,7 +64,7 @@ pub fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, body:
let mut check = Check::new(
CheckKind::UnusedLoopControlVariable(name.to_string()),
checker.locate_check(Range::from_located(expr)),
Range::from_located(expr),
);
if checker.patch() {
// Prefix the variable name with an underscore.

View File

@@ -1,14 +1,14 @@
use rustpython_ast::{Expr, Stmt, StmtKind};
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
pub fn useless_comparison(checker: &mut Checker, expr: &Expr, parent: &Stmt) {
if let StmtKind::Expr { .. } = &parent.node {
pub fn useless_comparison(checker: &mut Checker, expr: &Expr) {
if let ExprKind::Compare { left, .. } = &expr.node {
checker.add_check(Check::new(
CheckKind::UselessComparison,
checker.locate_check(Range::from_located(expr)),
Range::from_located(left),
));
}
}

View File

@@ -1,6 +1,6 @@
use rustpython_ast::{Constant, ExprKind, Stmt, StmtKind};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
@@ -12,7 +12,7 @@ pub fn useless_expression(checker: &mut Checker, body: &[Stmt]) {
ExprKind::List { .. } | ExprKind::Dict { .. } | ExprKind::Set { .. } => {
checker.add_check(Check::new(
CheckKind::UselessExpression,
checker.locate_check(Range::from_located(value)),
Range::from_located(value),
));
}
ExprKind::Constant { value: val, .. } => match &val {
@@ -20,7 +20,7 @@ pub fn useless_expression(checker: &mut Checker, body: &[Stmt]) {
_ => {
checker.add_check(Check::new(
CheckKind::UselessExpression,
checker.locate_check(Range::from_located(value)),
Range::from_located(value),
));
}
},

View File

@@ -1,28 +1,15 @@
use anyhow::Result;
use libcst_native::{
Arg, Call, Codegen, Dict, DictComp, DictElement, Element, Expr, Expression, LeftCurlyBrace,
LeftParen, LeftSquareBracket, List, ListComp, Module, Name, ParenthesizableWhitespace,
RightCurlyBrace, RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace,
SmallStatement, Statement, Tuple,
LeftParen, LeftSquareBracket, List, ListComp, Name, ParenthesizableWhitespace, RightCurlyBrace,
RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace, Tuple,
};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::cst::matchers::match_module;
use crate::cst::matchers::{match_expr, match_module};
use crate::source_code_locator::SourceCodeLocator;
fn match_expr<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Expr<'b>> {
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
if let Some(SmallStatement::Expr(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
Err(anyhow::anyhow!("Expected node to be: SmallStatement::Expr"))
}
} else {
Err(anyhow::anyhow!("Expected node to be: Statement::Simple"))
}
}
fn match_call<'a, 'b>(expr: &'a mut Expr<'b>) -> Result<&'a mut Call<'b>> {
if let Expression::Call(call) = &mut expr.value {
Ok(call)

View File

@@ -1,7 +1,7 @@
use log::error;
use rustpython_ast::{Expr, Stmt, StmtKind};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::autofix::helpers;
use crate::check_ast::Checker;
use crate::checks::CheckCode;
@@ -13,7 +13,7 @@ pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
func,
checker.settings.enabled.contains(&CheckCode::T201),
checker.settings.enabled.contains(&CheckCode::T203),
checker.locate_check(Range::from_located(expr)),
Range::from_located(expr),
) {
if checker.patch() {
let context = checker.binding_context();

View File

@@ -135,172 +135,3 @@ pub fn quotes(
None
}
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use rustpython_parser::lexer::LexResult;
use test_case::test_case;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::flake8_quotes::settings::Quote;
use crate::linter::tokenize;
use crate::{flake8_quotes, fs, linter, noqa, Settings, SourceCodeLocator};
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = tokenize(&contents);
let locator = SourceCodeLocator::new(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
linter::check_path(
path,
&contents,
tokens,
&locator,
&noqa_line_for,
settings,
autofix,
)
}
#[test_case(Path::new("doubles.py"))]
#[test_case(Path::new("doubles_escaped.py"))]
#[test_case(Path::new("doubles_multiline_string.py"))]
#[test_case(Path::new("doubles_noqa.py"))]
#[test_case(Path::new("doubles_wrapped.py"))]
fn doubles(path: &Path) -> Result<()> {
let snapshot = format!("doubles_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Single,
docstring_quotes: Quote::Single,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test_case(Path::new("singles.py"))]
#[test_case(Path::new("singles_escaped.py"))]
#[test_case(Path::new("singles_multiline_string.py"))]
#[test_case(Path::new("singles_noqa.py"))]
#[test_case(Path::new("singles_wrapped.py"))]
fn singles(path: &Path) -> Result<()> {
let snapshot = format!("singles_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Double,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Double,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test_case(Path::new("docstring_doubles.py"))]
#[test_case(Path::new("docstring_doubles_module_multiline.py"))]
#[test_case(Path::new("docstring_doubles_module_singleline.py"))]
#[test_case(Path::new("docstring_doubles_class.py"))]
#[test_case(Path::new("docstring_doubles_function.py"))]
#[test_case(Path::new("docstring_singles.py"))]
#[test_case(Path::new("docstring_singles_module_multiline.py"))]
#[test_case(Path::new("docstring_singles_module_singleline.py"))]
#[test_case(Path::new("docstring_singles_class.py"))]
#[test_case(Path::new("docstring_singles_function.py"))]
fn double_docstring(path: &Path) -> Result<()> {
let snapshot = format!("double_docstring_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Single,
docstring_quotes: Quote::Double,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test_case(Path::new("docstring_doubles.py"))]
#[test_case(Path::new("docstring_doubles_module_multiline.py"))]
#[test_case(Path::new("docstring_doubles_module_singleline.py"))]
#[test_case(Path::new("docstring_doubles_class.py"))]
#[test_case(Path::new("docstring_doubles_function.py"))]
#[test_case(Path::new("docstring_singles.py"))]
#[test_case(Path::new("docstring_singles_module_multiline.py"))]
#[test_case(Path::new("docstring_singles_module_singleline.py"))]
#[test_case(Path::new("docstring_singles_class.py"))]
#[test_case(Path::new("docstring_singles_function.py"))]
fn single_docstring(path: &Path) -> Result<()> {
let snapshot = format!("single_docstring_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Single,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
}

View File

@@ -1,2 +1,171 @@
pub mod checks;
pub mod settings;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use rustpython_parser::lexer::LexResult;
use test_case::test_case;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::flake8_quotes::settings::Quote;
use crate::linter::tokenize;
use crate::{flake8_quotes, fs, linter, noqa, Settings, SourceCodeLocator};
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = tokenize(&contents);
let locator = SourceCodeLocator::new(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
linter::check_path(
path,
&contents,
tokens,
&locator,
&noqa_line_for,
settings,
autofix,
)
}
#[test_case(Path::new("doubles.py"))]
#[test_case(Path::new("doubles_escaped.py"))]
#[test_case(Path::new("doubles_multiline_string.py"))]
#[test_case(Path::new("doubles_noqa.py"))]
#[test_case(Path::new("doubles_wrapped.py"))]
fn doubles(path: &Path) -> Result<()> {
let snapshot = format!("doubles_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Single,
docstring_quotes: Quote::Single,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test_case(Path::new("singles.py"))]
#[test_case(Path::new("singles_escaped.py"))]
#[test_case(Path::new("singles_multiline_string.py"))]
#[test_case(Path::new("singles_noqa.py"))]
#[test_case(Path::new("singles_wrapped.py"))]
fn singles(path: &Path) -> Result<()> {
let snapshot = format!("singles_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Double,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Double,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test_case(Path::new("docstring_doubles.py"))]
#[test_case(Path::new("docstring_doubles_module_multiline.py"))]
#[test_case(Path::new("docstring_doubles_module_singleline.py"))]
#[test_case(Path::new("docstring_doubles_class.py"))]
#[test_case(Path::new("docstring_doubles_function.py"))]
#[test_case(Path::new("docstring_singles.py"))]
#[test_case(Path::new("docstring_singles_module_multiline.py"))]
#[test_case(Path::new("docstring_singles_module_singleline.py"))]
#[test_case(Path::new("docstring_singles_class.py"))]
#[test_case(Path::new("docstring_singles_function.py"))]
fn double_docstring(path: &Path) -> Result<()> {
let snapshot = format!("double_docstring_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Single,
docstring_quotes: Quote::Double,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test_case(Path::new("docstring_doubles.py"))]
#[test_case(Path::new("docstring_doubles_module_multiline.py"))]
#[test_case(Path::new("docstring_doubles_module_singleline.py"))]
#[test_case(Path::new("docstring_doubles_class.py"))]
#[test_case(Path::new("docstring_doubles_function.py"))]
#[test_case(Path::new("docstring_singles.py"))]
#[test_case(Path::new("docstring_singles_module_multiline.py"))]
#[test_case(Path::new("docstring_singles_module_singleline.py"))]
#[test_case(Path::new("docstring_singles_class.py"))]
#[test_case(Path::new("docstring_singles_function.py"))]
fn single_docstring(path: &Path) -> Result<()> {
let snapshot = format!("single_docstring_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Single,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
}

View File

@@ -0,0 +1,50 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 5
column: 0
end_location:
row: 7
column: 3
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 16
column: 4
end_location:
row: 18
column: 7
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 21
column: 20
end_location:
row: 22
column: 37
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 30
column: 8
end_location:
row: 32
column: 11
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 35
column: 12
end_location:
row: 37
column: 15
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 3
column: 4
end_location:
row: 3
column: 27
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 5
column: 22
end_location:
row: 5
column: 43
fix: ~

View File

@@ -0,0 +1,50 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 3
column: 4
end_location:
row: 3
column: 26
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 11
column: 4
end_location:
row: 11
column: 26
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 15
column: 38
end_location:
row: 17
column: 3
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 17
column: 4
end_location:
row: 17
column: 19
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 21
column: 4
end_location:
row: 21
column: 27
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 4
column: 0
end_location:
row: 6
column: 3
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 9
column: 0
end_location:
row: 11
column: 3
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 2
column: 0
end_location:
row: 2
column: 31
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 6
column: 0
end_location:
row: 6
column: 31
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesDocstring: double
location:
row: 1
column: 0
end_location:
row: 3
column: 3
fix: ~
- kind:
BadQuotesDocstring: double
location:
row: 14
column: 4
end_location:
row: 16
column: 7
fix: ~
- kind:
BadQuotesDocstring: double
location:
row: 26
column: 8
end_location:
row: 28
column: 11
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesDocstring: double
location:
row: 2
column: 4
end_location:
row: 2
column: 53
fix: ~
- kind:
BadQuotesDocstring: double
location:
row: 6
column: 8
end_location:
row: 6
column: 57
fix: ~
- kind:
BadQuotesDocstring: double
location:
row: 9
column: 28
end_location:
row: 9
column: 52
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesDocstring: double
location:
row: 2
column: 4
end_location:
row: 2
column: 56
fix: ~
- kind:
BadQuotesDocstring: double
location:
row: 8
column: 4
end_location:
row: 10
column: 7
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesDocstring: double
location:
row: 1
column: 0
end_location:
row: 3
column: 3
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesDocstring: double
location:
row: 1
column: 0
end_location:
row: 1
column: 49
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesInlineString: single
location:
row: 1
column: 24
end_location:
row: 1
column: 45
fix: ~
- kind:
BadQuotesInlineString: single
location:
row: 2
column: 24
end_location:
row: 2
column: 46
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind: AvoidQuoteEscape
location:
row: 1
column: 25
end_location:
row: 1
column: 47
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 1
column: 4
end_location:
row: 3
column: 12
fix: ~

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesDocstring: single
location:
row: 1
column: 0
end_location:
row: 3
column: 3
fix: ~
- kind:
BadQuotesDocstring: single
location:
row: 12
column: 4
end_location:
row: 14
column: 7
fix: ~
- kind:
BadQuotesDocstring: single
location:
row: 24
column: 8
end_location:
row: 26
column: 11
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesDocstring: single
location:
row: 2
column: 4
end_location:
row: 2
column: 53
fix: ~
- kind:
BadQuotesDocstring: single
location:
row: 6
column: 8
end_location:
row: 6
column: 57
fix: ~
- kind:
BadQuotesDocstring: single
location:
row: 9
column: 28
end_location:
row: 9
column: 52
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesDocstring: single
location:
row: 2
column: 4
end_location:
row: 2
column: 56
fix: ~
- kind:
BadQuotesDocstring: single
location:
row: 8
column: 4
end_location:
row: 10
column: 7
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesDocstring: single
location:
row: 1
column: 0
end_location:
row: 3
column: 3
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesDocstring: single
location:
row: 1
column: 0
end_location:
row: 1
column: 49
fix: ~

View File

@@ -0,0 +1,59 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesMultilineString: double
location:
row: 5
column: 0
end_location:
row: 7
column: 3
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 11
column: 20
end_location:
row: 13
column: 3
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 18
column: 4
end_location:
row: 20
column: 7
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 23
column: 20
end_location:
row: 24
column: 37
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 32
column: 8
end_location:
row: 34
column: 11
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 37
column: 12
end_location:
row: 39
column: 15
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesMultilineString: double
location:
row: 3
column: 4
end_location:
row: 3
column: 27
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 5
column: 22
end_location:
row: 5
column: 43
fix: ~

View File

@@ -0,0 +1,50 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesMultilineString: double
location:
row: 3
column: 4
end_location:
row: 3
column: 26
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 11
column: 4
end_location:
row: 11
column: 26
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 15
column: 38
end_location:
row: 17
column: 3
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 17
column: 4
end_location:
row: 17
column: 19
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 21
column: 4
end_location:
row: 21
column: 27
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesMultilineString: double
location:
row: 4
column: 0
end_location:
row: 6
column: 3
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 9
column: 0
end_location:
row: 11
column: 3
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesMultilineString: double
location:
row: 2
column: 0
end_location:
row: 2
column: 31
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 6
column: 0
end_location:
row: 6
column: 31
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesInlineString: double
location:
row: 1
column: 24
end_location:
row: 1
column: 45
fix: ~
- kind:
BadQuotesInlineString: double
location:
row: 2
column: 24
end_location:
row: 2
column: 46
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind: AvoidQuoteEscape
location:
row: 1
column: 25
end_location:
row: 1
column: 47
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_quotes/mod.rs
expression: checks
---
- kind:
BadQuotesMultilineString: double
location:
row: 1
column: 4
end_location:
row: 3
column: 12
fix: ~

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ pub mod cli;
pub mod code_gen;
mod cst;
mod docstrings;
pub mod flake8_annotations;
mod flake8_bugbear;
mod flake8_builtins;
mod flake8_comprehensions;

View File

@@ -439,6 +439,10 @@ mod tests {
#[test_case(CheckCode::U006, Path::new("U006.py"); "U006")]
#[test_case(CheckCode::U007, Path::new("U007.py"); "U007")]
#[test_case(CheckCode::U008, Path::new("U008.py"); "U008")]
#[test_case(CheckCode::U009, Path::new("U009_0.py"); "U009_0")]
#[test_case(CheckCode::U009, Path::new("U009_1.py"); "U009_1")]
#[test_case(CheckCode::U009, Path::new("U009_2.py"); "U009_2")]
#[test_case(CheckCode::U009, Path::new("U009_3.py"); "U009_3")]
#[test_case(CheckCode::W292, Path::new("W292_0.py"); "W292_0")]
#[test_case(CheckCode::W292, Path::new("W292_1.py"); "W292_1")]
#[test_case(CheckCode::W292, Path::new("W292_2.py"); "W292_2")]

View File

@@ -1,4 +1,3 @@
use std::collections::BTreeMap;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
@@ -8,7 +7,6 @@ use std::time::Instant;
#[cfg(not(target_family = "wasm"))]
use ::ruff::cache;
use ::ruff::checks::{CheckCode, CheckKind};
use ::ruff::checks_gen::CheckCodePrefix;
use ::ruff::cli::{collect_per_file_ignores, extract_log_level, warn_on, Cli, Warnable};
use ::ruff::fs::iter_python_files;
use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin};
@@ -252,8 +250,6 @@ fn inner_main() -> Result<ExitCode> {
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect();
let per_file_ignores: BTreeMap<String, Vec<CheckCodePrefix>> =
collect_per_file_ignores(cli.per_file_ignores);
let mut configuration = Configuration::from_pyproject(&pyproject, &project_root)?;
if !exclude.is_empty() {
@@ -262,8 +258,9 @@ fn inner_main() -> Result<ExitCode> {
if !extend_exclude.is_empty() {
configuration.extend_exclude = extend_exclude;
}
if !per_file_ignores.is_empty() {
configuration.per_file_ignores = per_file_ignores;
if !cli.per_file_ignores.is_empty() {
configuration.per_file_ignores =
collect_per_file_ignores(cli.per_file_ignores, &project_root);
}
if !cli.select.is_empty() {
warn_on(

View File

@@ -1,8 +1,9 @@
use itertools::Itertools;
use rustpython_ast::{Arguments, Expr, ExprKind, Stmt};
use crate::ast::types::{FunctionScope, Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
use crate::pep8_naming::helpers;
use crate::pep8_naming::helpers::FunctionType;
use crate::pep8_naming::settings::Settings;
/// N801
@@ -25,7 +26,7 @@ pub fn invalid_class_name(class_def: &Stmt, name: &str) -> Option<Check> {
/// N802
pub fn invalid_function_name(func_def: &Stmt, name: &str, settings: &Settings) -> Option<Check> {
if !is_lower(name)
if name.to_lowercase() != name
&& !settings
.ignore_names
.iter()
@@ -40,8 +41,8 @@ pub fn invalid_function_name(func_def: &Stmt, name: &str, settings: &Settings) -
}
/// N803
pub fn invalid_argument_name(location: Range, name: &str) -> Option<Check> {
if !is_lower(name) {
pub fn invalid_argument_name(name: &str, location: Range) -> Option<Check> {
if name.to_lowercase() != name {
return Some(Check::new(
CheckKind::InvalidArgumentName(name.to_string()),
location,
@@ -53,21 +54,15 @@ pub fn invalid_argument_name(location: Range, name: &str) -> Option<Check> {
/// N804
pub fn invalid_first_argument_name_for_class_method(
scope: &Scope,
name: &str,
decorator_list: &[Expr],
args: &Arguments,
settings: &Settings,
) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Class) {
return None;
}
if decorator_list.iter().any(|decorator| {
if let ExprKind::Name { id, .. } = &decorator.node {
settings.classmethod_decorators.contains(id)
} else {
false
}
}) {
if matches!(
helpers::function_type(scope, name, decorator_list, settings),
FunctionType::ClassMethod
) {
if let Some(arg) = args.args.first() {
if arg.node.arg != "cls" {
return Some(Check::new(
@@ -83,31 +78,22 @@ pub fn invalid_first_argument_name_for_class_method(
/// N805
pub fn invalid_first_argument_name_for_method(
scope: &Scope,
name: &str,
decorator_list: &[Expr],
args: &Arguments,
settings: &Settings,
) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Class) {
return None;
}
if decorator_list.iter().any(|decorator| {
if let ExprKind::Name { id, .. } = &decorator.node {
settings.classmethod_decorators.contains(id)
|| settings.staticmethod_decorators.contains(id)
} else {
false
}
}) {
return None;
}
if let Some(arg) = args.args.first() {
if arg.node.arg != "self" {
return Some(Check::new(
CheckKind::InvalidFirstArgumentNameForMethod,
Range::from_located(arg),
));
if matches!(
helpers::function_type(scope, name, decorator_list, settings),
FunctionType::Method
) {
if let Some(arg) = args.args.first() {
if arg.node.arg != "self" {
return Some(Check::new(
CheckKind::InvalidFirstArgumentNameForMethod,
Range::from_located(arg),
));
}
}
}
None
@@ -128,18 +114,16 @@ pub fn non_lowercase_variable_in_function(scope: &Scope, expr: &Expr, name: &str
}
/// N807
pub fn dunder_function_name(func_def: &Stmt, scope: &Scope, name: &str) -> Option<Check> {
if matches!(scope.kind, ScopeKind::Class) {
pub fn dunder_function_name(scope: &Scope, stmt: &Stmt, name: &str) -> Option<Check> {
if matches!(scope.kind, ScopeKind::Class(_)) {
return None;
}
if name.starts_with("__") && name.ends_with("__") {
return Some(Check::new(
CheckKind::DunderFunctionName,
Range::from_located(func_def),
Range::from_located(stmt),
));
}
None
}
@@ -149,7 +133,7 @@ pub fn constant_imported_as_non_constant(
name: &str,
asname: &str,
) -> Option<Check> {
if is_upper(name) && !is_upper(asname) {
if helpers::is_upper(name) && !helpers::is_upper(asname) {
return Some(Check::new(
CheckKind::ConstantImportedAsNonConstant(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -164,7 +148,7 @@ pub fn lowercase_imported_as_non_lowercase(
name: &str,
asname: &str,
) -> Option<Check> {
if is_lower(name) && asname.to_lowercase() != asname {
if !helpers::is_upper(name) && helpers::is_lower(name) && asname.to_lowercase() != asname {
return Some(Check::new(
CheckKind::LowercaseImportedAsNonLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -179,7 +163,7 @@ pub fn camelcase_imported_as_lowercase(
name: &str,
asname: &str,
) -> Option<Check> {
if is_camelcase(name) && is_lower(asname) {
if helpers::is_camelcase(name) && helpers::is_lower(asname) {
return Some(Check::new(
CheckKind::CamelcaseImportedAsLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -194,7 +178,11 @@ pub fn camelcase_imported_as_constant(
name: &str,
asname: &str,
) -> Option<Check> {
if is_camelcase(name) && is_upper(asname) && !is_acronym(name, asname) {
if helpers::is_camelcase(name)
&& !helpers::is_lower(asname)
&& helpers::is_upper(asname)
&& !helpers::is_acronym(name, asname)
{
return Some(Check::new(
CheckKind::CamelcaseImportedAsConstant(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -205,10 +193,10 @@ pub fn camelcase_imported_as_constant(
/// N815
pub fn mixed_case_variable_in_class_scope(scope: &Scope, expr: &Expr, name: &str) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Class) {
if !matches!(scope.kind, ScopeKind::Class(_)) {
return None;
}
if is_mixed_case(name) {
if helpers::is_mixed_case(name) {
return Some(Check::new(
CheckKind::MixedCaseVariableInClassScope(name.to_string()),
Range::from_located(expr),
@@ -226,7 +214,7 @@ pub fn mixed_case_variable_in_global_scope(
if !matches!(scope.kind, ScopeKind::Module) {
return None;
}
if is_mixed_case(name) {
if helpers::is_mixed_case(name) {
return Some(Check::new(
CheckKind::MixedCaseVariableInGlobalScope(name.to_string()),
Range::from_located(expr),
@@ -241,7 +229,11 @@ pub fn camelcase_imported_as_acronym(
name: &str,
asname: &str,
) -> Option<Check> {
if is_camelcase(name) && is_upper(asname) && is_acronym(name, asname) {
if helpers::is_camelcase(name)
&& !helpers::is_lower(asname)
&& helpers::is_upper(asname)
&& helpers::is_acronym(name, asname)
{
return Some(Check::new(
CheckKind::CamelcaseImportedAsAcronym(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -272,101 +264,3 @@ pub fn error_suffix_on_exception_name(
}
None
}
fn is_lower(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_uppercase() {
return false;
} else if !cased && c.is_lowercase() {
cased = true;
}
}
cased
}
fn is_upper(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_lowercase() {
return false;
} else if (!cased) && c.is_uppercase() {
cased = true;
}
}
cased
}
fn is_camelcase(name: &str) -> bool {
!is_lower(name) && !is_upper(name) && !name.contains('_')
}
fn is_mixed_case(name: &str) -> bool {
!is_lower(name)
&& name
.strip_prefix('_')
.unwrap_or(name)
.chars()
.next()
.map_or_else(|| false, |c| c.is_lowercase())
}
fn is_acronym(name: &str, asname: &str) -> bool {
name.chars().filter(|c| c.is_uppercase()).join("") == asname
}
#[cfg(test)]
mod tests {
use super::{is_acronym, is_camelcase, is_lower, is_mixed_case, is_upper};
#[test]
fn test_is_lower() -> () {
assert!(is_lower("abc"));
assert!(is_lower("a_b_c"));
assert!(is_lower("a2c"));
assert!(!is_lower("aBc"));
assert!(!is_lower("ABC"));
assert!(!is_lower(""));
assert!(!is_lower("_"));
}
#[test]
fn test_is_upper() -> () {
assert!(is_upper("ABC"));
assert!(is_upper("A_B_C"));
assert!(is_upper("A2C"));
assert!(!is_upper("aBc"));
assert!(!is_upper("abc"));
assert!(!is_upper(""));
assert!(!is_upper("_"));
}
#[test]
fn test_is_camelcase() -> () {
assert!(is_camelcase("Camel"));
assert!(is_camelcase("CamelCase"));
assert!(!is_camelcase("camel"));
assert!(!is_camelcase("camel_case"));
assert!(!is_camelcase("CAMEL"));
assert!(!is_camelcase("CAMEL_CASE"));
}
#[test]
fn test_is_mixed_case() -> () {
assert!(is_mixed_case("mixedCase"));
assert!(is_mixed_case("mixed_Case"));
assert!(is_mixed_case("_mixed_Case"));
assert!(!is_mixed_case("mixed_case"));
assert!(!is_mixed_case("MIXED_CASE"));
assert!(!is_mixed_case(""));
assert!(!is_mixed_case("_"));
}
#[test]
fn test_is_acronym() -> () {
assert!(is_acronym("AB", "AB"));
assert!(is_acronym("AbcDef", "AD"));
assert!(!is_acronym("AbcDef", "Ad"));
assert!(!is_acronym("AbcDef", "AB"));
}
}

160
src/pep8_naming/helpers.rs Normal file
View File

@@ -0,0 +1,160 @@
use itertools::Itertools;
use rustpython_ast::{Expr, ExprKind};
use crate::ast::helpers::match_name_or_attr;
use crate::ast::types::{Scope, ScopeKind};
use crate::pep8_naming::settings::Settings;
const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"];
const METACLASS_BASES: [&str; 2] = ["type", "ABCMeta"];
pub enum FunctionType {
Function,
Method,
ClassMethod,
StaticMethod,
}
/// Classify a function based on its scope, name, and decorators.
pub fn function_type(
scope: &Scope,
name: &str,
decorator_list: &[Expr],
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 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
}
}) {
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`).
FunctionType::StaticMethod
} else {
// It's an instance method.
FunctionType::Method
}
} else {
FunctionType::Function
}
}
pub fn is_lower(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_uppercase() {
return false;
} else if !cased && c.is_lowercase() {
cased = true;
}
}
cased
}
pub fn is_upper(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_lowercase() {
return false;
} else if !cased && c.is_uppercase() {
cased = true;
}
}
cased
}
pub fn is_camelcase(name: &str) -> bool {
!is_lower(name) && !is_upper(name) && !name.contains('_')
}
pub fn is_mixed_case(name: &str) -> bool {
!is_lower(name)
&& name
.strip_prefix('_')
.unwrap_or(name)
.chars()
.next()
.map_or_else(|| false, |c| c.is_lowercase())
}
pub fn is_acronym(name: &str, asname: &str) -> bool {
name.chars().filter(|c| c.is_uppercase()).join("") == asname
}
#[cfg(test)]
mod tests {
use crate::pep8_naming::helpers::{
is_acronym, is_camelcase, is_lower, is_mixed_case, is_upper,
};
#[test]
fn test_is_lower() -> () {
assert!(is_lower("abc"));
assert!(is_lower("a_b_c"));
assert!(is_lower("a2c"));
assert!(!is_lower("aBc"));
assert!(!is_lower("ABC"));
assert!(!is_lower(""));
assert!(!is_lower("_"));
}
#[test]
fn test_is_upper() -> () {
assert!(is_upper("ABC"));
assert!(is_upper("A_B_C"));
assert!(is_upper("A2C"));
assert!(!is_upper("aBc"));
assert!(!is_upper("abc"));
assert!(!is_upper(""));
assert!(!is_upper("_"));
}
#[test]
fn test_is_camelcase() -> () {
assert!(is_camelcase("Camel"));
assert!(is_camelcase("CamelCase"));
assert!(!is_camelcase("camel"));
assert!(!is_camelcase("camel_case"));
assert!(!is_camelcase("CAMEL"));
assert!(!is_camelcase("CAMEL_CASE"));
}
#[test]
fn test_is_mixed_case() -> () {
assert!(is_mixed_case("mixedCase"));
assert!(is_mixed_case("mixed_Case"));
assert!(is_mixed_case("_mixed_Case"));
assert!(!is_mixed_case("mixed_case"));
assert!(!is_mixed_case("MIXED_CASE"));
assert!(!is_mixed_case(""));
assert!(!is_mixed_case("_"));
}
#[test]
fn test_is_acronym() -> () {
assert!(is_acronym("AB", "AB"));
assert!(is_acronym("AbcDef", "AD"));
assert!(!is_acronym("AbcDef", "Ad"));
assert!(!is_acronym("AbcDef", "AB"));
}
}

View File

@@ -1,2 +1,3 @@
pub mod checks;
mod helpers;
pub mod settings;

View File

@@ -2,7 +2,7 @@ use itertools::izip;
use rustpython_ast::Location;
use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Unaryop};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind, RejectedCmpop};
use crate::source_code_locator::SourceCodeLocator;
@@ -61,7 +61,6 @@ pub fn not_tests(
operand: &Expr,
check_not_in: bool,
check_not_is: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -73,7 +72,7 @@ pub fn not_tests(
if check_not_in {
checks.push(Check::new(
CheckKind::NotInTest,
locator.locate_check(Range::from_located(operand)),
Range::from_located(operand),
));
}
}
@@ -81,7 +80,7 @@ pub fn not_tests(
if check_not_is {
checks.push(Check::new(
CheckKind::NotIsTest,
locator.locate_check(Range::from_located(operand)),
Range::from_located(operand),
));
}
}
@@ -101,7 +100,6 @@ pub fn literal_comparisons(
comparators: &[Expr],
check_none_comparisons: bool,
check_true_false_comparisons: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -121,13 +119,13 @@ pub fn literal_comparisons(
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::Eq),
locator.locate_check(Range::from_located(comparator)),
Range::from_located(comparator),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::NotEq),
locator.locate_check(Range::from_located(comparator)),
Range::from_located(comparator),
));
}
}
@@ -141,13 +139,13 @@ pub fn literal_comparisons(
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
locator.locate_check(Range::from_located(comparator)),
Range::from_located(comparator),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
locator.locate_check(Range::from_located(comparator)),
Range::from_located(comparator),
));
}
}
@@ -167,13 +165,13 @@ pub fn literal_comparisons(
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::Eq),
locator.locate_check(Range::from_located(comparator)),
Range::from_located(comparator),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::NotEq),
locator.locate_check(Range::from_located(comparator)),
Range::from_located(comparator),
));
}
}
@@ -187,13 +185,13 @@ pub fn literal_comparisons(
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
locator.locate_check(Range::from_located(comparator)),
Range::from_located(comparator),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
locator.locate_check(Range::from_located(comparator)),
Range::from_located(comparator),
));
}
}

View File

@@ -13,7 +13,7 @@ use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::docstrings::helpers;
use crate::docstrings::sections::{section_contexts, SectionContext};
use crate::docstrings::styles::SectionStyle;
use crate::visibility::{is_init, is_magic, is_overload, is_static, Visibility};
use crate::visibility::{is_init, is_magic, is_overload, is_staticmethod, Visibility};
/// D100, D101, D102, D103, D104, D105, D106, D107
pub fn not_missing(
@@ -1308,7 +1308,8 @@ fn missing_args(checker: &mut Checker, definition: &Definition, docstrings_args:
.skip(
// If this is a non-static method, skip `cls` or `self`.
usize::from(
matches!(definition.kind, DefinitionKind::Method(_)) && !is_static(parent),
matches!(definition.kind, DefinitionKind::Method(_))
&& !is_staticmethod(parent),
),
)
.collect();

View File

@@ -1,13 +1,11 @@
use std::collections::BTreeSet;
use itertools::izip;
use regex::Regex;
use rustpython_parser::ast::{
Arg, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt,
StmtKind,
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind,
};
use crate::ast::types::{BindingKind, CheckLocator, FunctionScope, Range, Scope, ScopeKind};
use crate::ast::types::{BindingKind, FunctionScope, Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
/// F631
@@ -30,12 +28,30 @@ pub fn if_tuple(test: &Expr, location: Range) -> Option<Check> {
None
}
/// F821
pub fn undefined_local(scopes: &[&Scope], name: &str) -> Option<Check> {
let current = &scopes.last().expect("No current scope found.");
if matches!(current.kind, ScopeKind::Function(_)) && !current.values.contains_key(name) {
for scope in scopes.iter().rev().skip(1) {
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if let Some(binding) = scope.values.get(name) {
if let Some((scope_id, location)) = binding.used {
if scope_id == current.id {
return Some(Check::new(
CheckKind::UndefinedLocal(name.to_string()),
location,
));
}
}
}
}
}
}
None
}
/// F841
pub fn unused_variables(
scope: &Scope,
locator: &dyn CheckLocator,
dummy_variable_rgx: &Regex,
) -> Vec<Check> {
pub fn unused_variables(scope: &Scope, dummy_variable_rgx: &Regex) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
if matches!(
@@ -55,7 +71,7 @@ pub fn unused_variables(
{
checks.push(Check::new(
CheckKind::UnusedVariable(name.to_string()),
locator.locate_check(binding.range),
binding.range,
));
}
}
@@ -131,7 +147,6 @@ pub fn repeated_keys(
keys: &[Expr],
check_repeated_literals: bool,
check_repeated_variables: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -146,7 +161,7 @@ pub fn repeated_keys(
if check_repeated_literals && v1 == v2 {
checks.push(Check::new(
CheckKind::MultiValueRepeatedKeyLiteral,
locator.locate_check(Range::from_located(k2)),
Range::from_located(k2),
))
}
}
@@ -154,7 +169,7 @@ pub fn repeated_keys(
if check_repeated_variables && v1 == v2 {
checks.push(Check::new(
CheckKind::MultiValueRepeatedKeyVariable((*v2).to_string()),
locator.locate_check(Range::from_located(k2)),
Range::from_located(k2),
))
}
}
@@ -166,45 +181,6 @@ pub fn repeated_keys(
checks
}
fn is_constant(expr: &Expr) -> bool {
match &expr.node {
ExprKind::Constant { .. } => true,
ExprKind::Tuple { elts, .. } => elts.iter().all(is_constant),
_ => false,
}
}
fn is_singleton(expr: &Expr) -> bool {
matches!(
expr.node,
ExprKind::Constant {
value: Constant::None | Constant::Bool(_) | Constant::Ellipsis,
..
}
)
}
fn is_constant_non_singleton(expr: &Expr) -> bool {
is_constant(expr) && !is_singleton(expr)
}
/// F632
pub fn is_literal(left: &Expr, ops: &[Cmpop], comparators: &[Expr], location: Range) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
let mut left = left;
for (op, right) in izip!(ops, comparators) {
if matches!(op, Cmpop::Is | Cmpop::IsNot)
&& (is_constant_non_singleton(left) || is_constant_non_singleton(right))
{
checks.push(Check::new(CheckKind::IsLiteral, location));
}
left = right;
}
checks
}
/// F621, F622
pub fn starred_expressions(
elts: &[Expr],
@@ -236,12 +212,7 @@ pub fn starred_expressions(
}
/// F701
pub fn break_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
parent_stack: &[usize],
locator: &dyn CheckLocator,
) -> Option<Check> {
pub fn break_outside_loop(stmt: &Stmt, parents: &[&Stmt], parent_stack: &[usize]) -> Option<Check> {
let mut allowed: bool = false;
let mut parent = stmt;
for index in parent_stack.iter().rev() {
@@ -269,7 +240,7 @@ pub fn break_outside_loop(
if !allowed {
Some(Check::new(
CheckKind::BreakOutsideLoop,
locator.locate_check(Range::from_located(stmt)),
Range::from_located(stmt),
))
} else {
None
@@ -281,7 +252,6 @@ pub fn continue_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
parent_stack: &[usize],
locator: &dyn CheckLocator,
) -> Option<Check> {
let mut allowed: bool = false;
let mut parent = stmt;
@@ -310,7 +280,7 @@ pub fn continue_outside_loop(
if !allowed {
Some(Check::new(
CheckKind::ContinueOutsideLoop,
locator.locate_check(Range::from_located(stmt)),
Range::from_located(stmt),
))
} else {
None

View File

@@ -1,11 +1,14 @@
use anyhow::Result;
use libcst_native::{Codegen, ImportNames, NameOrAttribute, SmallStatement, Statement};
use libcst_native::{
Codegen, CompOp, Comparison, ComparisonTarget, Expr, Expression, ImportNames, NameOrAttribute,
SmallStatement, Statement,
};
use rustpython_ast::Stmt;
use crate::ast::types::Range;
use crate::autofix::{helpers, Fix};
use crate::cst::helpers::compose_module_path;
use crate::cst::matchers::match_module;
use crate::cst::matchers::{match_expr, match_module};
use crate::source_code_locator::SourceCodeLocator;
/// Generate a Fix to remove any unused imports from an `import` statement.
@@ -138,3 +141,66 @@ pub fn remove_unused_import_froms(
))
}
}
fn match_comparison<'a, 'b>(expr: &'a mut Expr<'b>) -> Result<&'a mut Comparison<'b>> {
if let Expression::Comparison(comparison) = &mut expr.value {
Ok(comparison)
} else {
Err(anyhow::anyhow!(
"Expected node to be: Expression::Comparison"
))
}
}
/// Generate a Fix to replace invalid is/is not comparisons with equal/not equal
pub fn fix_invalid_literal_comparison(locator: &SourceCodeLocator, location: Range) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&location);
let mut tree = match_module(&module_text)?;
let mut expr = match_expr(&mut tree)?;
let cmp = match_comparison(expr)?;
let target = cmp
.comparisons
.get(0)
.ok_or_else(|| anyhow::anyhow!("Expected one ComparisonTarget"))?;
let new_operator = match &target.operator {
CompOp::Is {
whitespace_before: b,
whitespace_after: a,
} => Ok(CompOp::Equal {
whitespace_before: b.clone(),
whitespace_after: a.clone(),
}),
CompOp::IsNot {
whitespace_before: b,
whitespace_after: a,
whitespace_between: _,
} => Ok(CompOp::NotEqual {
whitespace_before: b.clone(),
whitespace_after: a.clone(),
}),
op => Err(anyhow::anyhow!(
"Unexpected operator: {:?}. Expected Is or IsNot.",
op
)),
}?;
expr.value = Expression::Comparison(Box::new(Comparison {
left: cmp.left.clone(),
comparisons: vec![ComparisonTarget {
operator: new_operator,
comparator: target.comparator.clone(),
}],
lpar: cmp.lpar.clone(),
rpar: cmp.rpar.clone(),
}));
let mut state = Default::default();
tree.codegen(&mut state);
Ok(Fix::replacement(
state.to_string(),
location.location,
location.end_location,
))
}

View File

@@ -1,13 +1,12 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::pyflakes::checks;
/// F631
pub fn assert_tuple(checker: &mut Checker, stmt: &Stmt, test: &Expr) {
if let Some(check) = checks::assert_tuple(test, checker.locate_check(Range::from_located(stmt)))
{
if let Some(check) = checks::assert_tuple(test, Range::from_located(stmt)) {
checker.add_check(check);
}
}

View File

@@ -1,12 +1,12 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::types::{CheckLocator, Range};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::pyflakes::checks;
/// F634
pub fn if_tuple(checker: &mut Checker, stmt: &Stmt, test: &Expr) {
if let Some(check) = checks::if_tuple(test, checker.locate_check(Range::from_located(stmt))) {
if let Some(check) = checks::if_tuple(test, Range::from_located(stmt)) {
checker.add_check(check);
}
}

View File

@@ -0,0 +1,62 @@
use itertools::izip;
use log::error;
use rustpython_ast::{Cmpop, Constant, Expr, ExprKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::pyflakes::fixes::fix_invalid_literal_comparison;
fn is_singleton(expr: &Expr) -> bool {
matches!(
expr.node,
ExprKind::Constant {
value: Constant::None | Constant::Bool(_) | Constant::Ellipsis,
..
}
)
}
fn is_constant(expr: &Expr) -> bool {
match &expr.node {
ExprKind::Constant { .. } => true,
ExprKind::Tuple { elts, .. } => elts.iter().all(is_constant),
_ => false,
}
}
fn is_constant_non_singleton(expr: &Expr) -> bool {
is_constant(expr) && !is_singleton(expr)
}
/// F632
pub fn invalid_literal_comparison(
checker: &mut Checker,
left: &Expr,
ops: &[Cmpop],
comparators: &[Expr],
location: Range,
) {
let mut left = left;
for (op, right) in izip!(ops, comparators) {
if matches!(op, Cmpop::Is | Cmpop::IsNot)
&& (is_constant_non_singleton(left) || is_constant_non_singleton(right))
{
let mut check = Check::new(CheckKind::IsLiteral, location);
if checker.patch() {
match fix_invalid_literal_comparison(
checker.locator,
Range {
location: left.location,
end_location: right.end_location.unwrap(),
},
) {
Ok(fix) => check.amend(fix),
Err(e) => error!("Failed to fix invalid comparison: {}", e),
}
}
checker.add_check(check);
}
left = right;
}
}

Some files were not shown because too many files have changed in this diff Show More