diff --git a/LICENSE b/LICENSE index f328ba5db2..b297cf072f 100644 --- a/LICENSE +++ b/LICENSE @@ -245,6 +245,31 @@ are: SOFTWARE. """ +- flake8-pyi, licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2016 Ɓukasz Langa + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + - flake8-print, licensed as follows: """ MIT License diff --git a/README.md b/README.md index c37868c1fa..5b827168dd 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ This README is also available as [documentation](https://beta.ruff.rs/docs/). 1. [flake8-no-pep420 (INP)](#flake8-no-pep420-inp) 1. [flake8-pie (PIE)](#flake8-pie-pie) 1. [flake8-print (T20)](#flake8-print-t20) + 1. [flake8-pyi (PYI)](#flake8-pyi-pyi) 1. [flake8-pytest-style (PT)](#flake8-pytest-style-pt) 1. [flake8-quotes (Q)](#flake8-quotes-q) 1. [flake8-return (RET)](#flake8-return-ret) @@ -1142,6 +1143,14 @@ For more, see [flake8-print](https://pypi.org/project/flake8-print/) on PyPI. | T201 | print-found | `print` found | | | T203 | p-print-found | `pprint` found | | +### flake8-pyi (PYI) + +For more, see [flake8-pyi](https://pypi.org/project/flake8-pyi/) on PyPI. + +| Code | Name | Message | Fix | +| ---- | ---- | ------- | --- | +| [PYI001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/prefix-type-params.md) | [prefix-type-params](https://github.com/charliermarsh/ruff/blob/main/docs/rules/prefix-type-params.md) | Name of private `{kind}` must start with _ | | + ### flake8-pytest-style (PT) For more, see [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/) on PyPI. @@ -1701,6 +1710,7 @@ natively, including: * [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420) * [flake8-pie](https://pypi.org/project/flake8-pie/) * [flake8-print](https://pypi.org/project/flake8-print/) +* [flake8-pyi](https://pypi.org/project/flake8-pyi/) * [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/) * [flake8-quotes](https://pypi.org/project/flake8-quotes/) * [flake8-raise](https://pypi.org/project/flake8-raise/) diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI001.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI001.py new file mode 100644 index 0000000000..2841ceb25c --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI001.py @@ -0,0 +1,13 @@ +from typing import ParamSpec, TypeVar, TypeVarTuple + +T = TypeVar("T") # OK + +TTuple = TypeVarTuple("TTuple") # OK + +P = ParamSpec("P") # OK + +_T = TypeVar("_T") # OK + +_TTuple = TypeVarTuple("_TTuple") # OK + +_P = ParamSpec("_P") # OK diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI001.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI001.pyi new file mode 100644 index 0000000000..05956fc48f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI001.pyi @@ -0,0 +1,13 @@ +from typing import ParamSpec, TypeVar, TypeVarTuple + +T = TypeVar("T") # Error: TypeVars in stubs must start with _ + +TTuple = TypeVarTuple("TTuple") # Error: TypeVarTuples must also start with _ + +P = ParamSpec("P") # Error: ParamSpecs must start with _ + +_T = TypeVar("_T") # OK + +_TTuple = TypeVarTuple("_TTuple") # OK + +_P = ParamSpec("_P") # OK diff --git a/crates/ruff/src/checkers/ast.rs b/crates/ruff/src/checkers/ast.rs index e9ecc63506..2481c2970a 100644 --- a/crates/ruff/src/checkers/ast.rs +++ b/crates/ruff/src/checkers/ast.rs @@ -36,10 +36,10 @@ use crate::rules::{ flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format, - flake8_pie, flake8_print, flake8_pytest_style, flake8_raise, flake8_return, flake8_self, - flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, - flake8_use_pathlib, mccabe, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes, - pygrep_hooks, pylint, pyupgrade, ruff, tryceratops, + flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, + flake8_self, flake8_simplify, flake8_tidy_imports, flake8_type_checking, + flake8_unused_arguments, flake8_use_pathlib, mccabe, pandas_vet, pep8_naming, pycodestyle, + pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops, }; use crate::settings::types::PythonVersion; use crate::settings::{flags, Settings}; @@ -1713,6 +1713,12 @@ where } } + if self.settings.rules.enabled(&Rule::PrefixTypeParams) { + if self.path.extension().map_or(false, |ext| ext == "pyi") { + flake8_pyi::rules::prefix_type_params(self, value, targets); + } + } + if self.settings.rules.enabled(&Rule::UselessMetaclassType) { pyupgrade::rules::useless_metaclass_type(self, stmt, value, targets); } diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 3dac81d97c..cfc4aff296 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -442,6 +442,8 @@ ruff_macros::define_rule_mapping!( EM101 => rules::flake8_errmsg::rules::RawStringInException, EM102 => rules::flake8_errmsg::rules::FStringInException, EM103 => rules::flake8_errmsg::rules::DotFormatInException, + // flake8-pyi + PYI001 => rules::flake8_pyi::rules::PrefixTypeParams, // flake8-pytest-style PT001 => rules::flake8_pytest_style::rules::IncorrectFixtureParenthesesStyle, PT002 => rules::flake8_pytest_style::rules::FixturePositionalArgs, @@ -632,6 +634,9 @@ pub enum Linter { /// [flake8-print](https://pypi.org/project/flake8-print/) #[prefix = "T20"] Flake8Print, + /// [flake8-pyi](https://pypi.org/project/flake8-pyi/) + #[prefix = "PYI"] + Flake8Pyi, /// [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/) #[prefix = "PT"] Flake8PytestStyle, diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs new file mode 100644 index 0000000000..08ca62cd29 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -0,0 +1,26 @@ +//! Rules from [flake8-pyi](https://pypi.org/project/flake8-pyi/). +pub(crate) mod rules; + +#[cfg(test)] +mod tests { + use std::path::Path; + + use anyhow::Result; + use test_case::test_case; + + use crate::registry::Rule; + use crate::test::test_path; + use crate::{assert_yaml_snapshot, settings}; + + #[test_case(Rule::PrefixTypeParams, Path::new("PYI001.pyi"))] + #[test_case(Rule::PrefixTypeParams, Path::new("PYI001.py"))] + fn rules(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("flake8_pyi").join(path).as_path(), + &settings::Settings::for_rule(rule_code), + )?; + assert_yaml_snapshot!(snapshot, diagnostics); + Ok(()) + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules.rs b/crates/ruff/src/rules/flake8_pyi/rules.rs new file mode 100644 index 0000000000..4c94ab11d4 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules.rs @@ -0,0 +1,92 @@ +use ruff_macros::{define_violation, derive_message_formats}; +use rustpython_parser::ast::{Expr, ExprKind}; +use serde::{Deserialize, Serialize}; +use std::fmt; + +use crate::ast::types::Range; +use crate::checkers::ast::Checker; +use crate::registry::Diagnostic; +use crate::violation::Violation; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum VarKind { + TypeVar, + ParamSpec, + TypeVarTuple, +} + +impl fmt::Display for VarKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + VarKind::TypeVar => fmt.write_str("TypeVar"), + VarKind::ParamSpec => fmt.write_str("ParamSpec"), + VarKind::TypeVarTuple => fmt.write_str("TypeVarTuple"), + } + } +} + +define_violation!( + /// ### What it does + /// Checks that type `TypeVar`, `ParamSpec`, and `TypeVarTuple` definitions in + /// stubs are prefixed with `_`. + /// + /// ### Why is this bad? + /// By prefixing type parameters with `_`, we can avoid accidentally exposing + /// names internal to the stub. + /// + /// ### Example + /// ```python + /// from typing import TypeVar + /// + /// T = TypeVar("T") + /// ``` + /// + /// Use instead: + /// ```python + /// from typing import TypeVar + /// + /// _T = TypeVar("_T") + /// ``` + pub struct PrefixTypeParams { + pub kind: VarKind, + } +); +impl Violation for PrefixTypeParams { + #[derive_message_formats] + fn message(&self) -> String { + let PrefixTypeParams { kind } = self; + format!("Name of private `{kind}` must start with _") + } +} + +/// PYI001 +pub fn prefix_type_params(checker: &mut Checker, value: &Expr, targets: &[Expr]) { + if targets.len() != 1 { + return; + } + if let ExprKind::Name { id, .. } = &targets[0].node { + if id.starts_with('_') { + return; + } + }; + + if let ExprKind::Call { func, .. } = &value.node { + let Some(kind) = checker.resolve_call_path(func).and_then(|call_path| { + if checker.match_typing_call_path(&call_path, "ParamSpec") { + Some(VarKind::ParamSpec) + } else if checker.match_typing_call_path(&call_path, "TypeVar") { + Some(VarKind::TypeVar) + } else if checker.match_typing_call_path(&call_path, "TypeVarTuple") { + Some(VarKind::TypeVarTuple) + } else { + None + } + }) else { + return; + }; + checker.diagnostics.push(Diagnostic::new( + PrefixTypeParams { kind }, + Range::from_located(value), + )); + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI001_PYI001.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI001_PYI001.py.snap new file mode 100644 index 0000000000..efcc2d0c99 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI001_PYI001.py.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +expression: diagnostics +--- +[] + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI001_PYI001.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI001_PYI001.pyi.snap new file mode 100644 index 0000000000..4e7e410b38 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI001_PYI001.pyi.snap @@ -0,0 +1,38 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +expression: diagnostics +--- +- kind: + PrefixTypeParams: + kind: TypeVar + location: + row: 3 + column: 4 + end_location: + row: 3 + column: 16 + fix: ~ + parent: ~ +- kind: + PrefixTypeParams: + kind: TypeVarTuple + location: + row: 5 + column: 9 + end_location: + row: 5 + column: 31 + fix: ~ + parent: ~ +- kind: + PrefixTypeParams: + kind: ParamSpec + location: + row: 7 + column: 4 + end_location: + row: 7 + column: 18 + fix: ~ + parent: ~ + diff --git a/crates/ruff/src/rules/mod.rs b/crates/ruff/src/rules/mod.rs index 2784888c14..4acbceaa2b 100644 --- a/crates/ruff/src/rules/mod.rs +++ b/crates/ruff/src/rules/mod.rs @@ -19,6 +19,7 @@ pub mod flake8_logging_format; pub mod flake8_no_pep420; pub mod flake8_pie; pub mod flake8_print; +pub mod flake8_pyi; pub mod flake8_pytest_style; pub mod flake8_quotes; pub mod flake8_raise; diff --git a/docs/rules/prefix-type-params.md b/docs/rules/prefix-type-params.md new file mode 100644 index 0000000000..b72095d97d --- /dev/null +++ b/docs/rules/prefix-type-params.md @@ -0,0 +1,25 @@ +# prefix-type-params (PYI001) + +Derived from the **flake8-pyi** linter. + +### What it does +Checks that type `TypeVar`, `ParamSpec`, and `TypeVarTuple` definitions in +stubs are prefixed with `_`. + +### Why is this bad? +By prefixing type parameters with `_`, we can avoid accidentally exposing +names internal to the stub. + +### Example +```python +from typing import TypeVar + +T = TypeVar("T") +``` + +Use instead: +```python +from typing import TypeVar + +_T = TypeVar("_T") +``` \ No newline at end of file diff --git a/ruff.schema.json b/ruff.schema.json index b3122779d9..41c30f310b 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1790,6 +1790,10 @@ "PTH122", "PTH123", "PTH124", + "PYI", + "PYI0", + "PYI00", + "PYI001", "Q", "Q0", "Q00",