Use Jupyter mode while parsing Notebook files (#5552)

## Summary

Enable using the new `Mode::Jupyter` for the tokenizer/parser to parse
Jupyter line magic tokens.

The individual call to the lexer i.e., `lex_starts_at` done by various
rules should consider the context of the source code (is this content
from a Jupyter Notebook?). Thus, a new field `source_type` (of type
`PySourceType`) is added to `Checker` which is being passed around as an
argument to the relevant functions. This is then used to determine the
`Mode` for the lexer.

## Test Plan

Add new test cases to make sure that the magic statement is considered
while generating the diagnostic and autofix:
* For `I001`, if there's a magic statement in between two import blocks,
they should be sorted independently

fixes: #6090
This commit is contained in:
Dhruv Manilawala 2023-08-05 06:02:07 +05:30 committed by GitHub
parent d788957ec4
commit 32fa05765a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 652 additions and 196 deletions

View file

@ -0,0 +1,8 @@
{
"execution_count": null,
"cell_type": "code",
"id": "1",
"metadata": {},
"outputs": [],
"source": ["%%timeit\n", "print('hello world')"]
}

View file

@ -25,6 +25,23 @@
"def foo():\n", "def foo():\n",
" pass" " pass"
] ]
},
{
"cell_type": "code",
"execution_count": null,
"id": "16214f6f-bb32-4594-81be-79fb27c6ec92",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import sys\n",
"\n",
"%matplotlib \\\n",
" --inline\n",
"\n",
"import math\n",
"import abc"
]
} }
], ],
"metadata": { "metadata": {

View file

@ -27,6 +27,23 @@
"def foo():\n", "def foo():\n",
" pass" " pass"
] ]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6d6c55c6-4a34-4662-914b-4ee11c9c24a5",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"from pathlib import Path\n",
"\n",
"%matplotlib \\\n",
" --inline\n",
"\n",
"import abc\n",
"import math"
]
} }
], ],
"metadata": { "metadata": {

View file

@ -0,0 +1,52 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "eab4754a-d6df-4b41-8ee8-7e23aef440f9",
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"\n",
"%matplotlib inline\n",
"\n",
"import os\n",
"\n",
"_ = math.pi"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2b0e2986-1b87-4bb6-9b1d-c11ca1decd87",
"metadata": {},
"outputs": [],
"source": [
"%%timeit\n",
"import sys"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View file

@ -0,0 +1,51 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "cad32845-44f9-4a53-8b8c-a6b1bb3f3378",
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"\n",
"%matplotlib inline\n",
"\n",
"\n",
"_ = math.pi"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d7b8e967-8b4a-493b-b6f7-d5cecfb3a5c3",
"metadata": {},
"outputs": [],
"source": [
"%%timeit\n",
"import sys"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View file

@ -3,10 +3,12 @@
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use ruff_diagnostics::Edit; use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Ranged, Stmt}; use ruff_python_ast::{
self as ast, Arguments, ExceptHandler, Expr, Keyword, PySourceType, Ranged, Stmt,
};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::{lexer, Mode}; use ruff_python_parser::{lexer, AsMode};
use ruff_python_trivia::{has_leading_content, is_python_whitespace, PythonWhitespace}; use ruff_python_trivia::{has_leading_content, is_python_whitespace, PythonWhitespace};
use ruff_source_file::{Locator, NewlineWithTrailingNewline}; use ruff_source_file::{Locator, NewlineWithTrailingNewline};
use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_text_size::{TextLen, TextRange, TextSize};
@ -88,6 +90,7 @@ pub(crate) fn remove_argument<T: Ranged>(
arguments: &Arguments, arguments: &Arguments,
parentheses: Parentheses, parentheses: Parentheses,
locator: &Locator, locator: &Locator,
source_type: PySourceType,
) -> Result<Edit> { ) -> Result<Edit> {
// TODO(sbrugman): Preserve trailing comments. // TODO(sbrugman): Preserve trailing comments.
if arguments.keywords.len() + arguments.args.len() > 1 { if arguments.keywords.len() + arguments.args.len() > 1 {
@ -106,7 +109,7 @@ pub(crate) fn remove_argument<T: Ranged>(
let mut seen_comma = false; let mut seen_comma = false;
for (tok, range) in lexer::lex_starts_at( for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()), locator.slice(arguments.range()),
Mode::Module, source_type.as_mode(),
arguments.start(), arguments.start(),
) )
.flatten() .flatten()
@ -135,7 +138,7 @@ pub(crate) fn remove_argument<T: Ranged>(
// previous comma to the end of the argument. // previous comma to the end of the argument.
for (tok, range) in lexer::lex_starts_at( for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()), locator.slice(arguments.range()),
Mode::Module, source_type.as_mode(),
arguments.start(), arguments.start(),
) )
.flatten() .flatten()

View file

@ -37,7 +37,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// Identify any valid runtime imports. If a module is imported at runtime, and // Identify any valid runtime imports. If a module is imported at runtime, and
// used at runtime, then by default, we avoid flagging any other // used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only. // imports from that model as typing-only.
let enforce_typing_imports = !checker.is_stub let enforce_typing_imports = !checker.source_type.is_stub()
&& checker.any_enabled(&[ && checker.any_enabled(&[
Rule::RuntimeImportInTypeCheckingBlock, Rule::RuntimeImportInTypeCheckingBlock,
Rule::TypingOnlyFirstPartyImport, Rule::TypingOnlyFirstPartyImport,
@ -243,7 +243,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pyflakes::rules::unused_annotation(checker, scope, &mut diagnostics); pyflakes::rules::unused_annotation(checker, scope, &mut diagnostics);
} }
if !checker.is_stub { if !checker.source_type.is_stub() {
if checker.any_enabled(&[ if checker.any_enabled(&[
Rule::UnusedClassMethodArgument, Rule::UnusedClassMethodArgument,
Rule::UnusedFunctionArgument, Rule::UnusedFunctionArgument,

View file

@ -30,7 +30,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::MissingTypeKwargs, Rule::MissingTypeKwargs,
Rule::MissingTypeSelf, Rule::MissingTypeSelf,
]); ]);
let enforce_stubs = checker.is_stub && checker.enabled(Rule::DocstringInStub); let enforce_stubs = checker.source_type.is_stub() && checker.enabled(Rule::DocstringInStub);
let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable); let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable);
let enforce_docstrings = checker.any_enabled(&[ let enforce_docstrings = checker.any_enabled(&[
Rule::BlankLineAfterLastSection, Rule::BlankLineAfterLastSection,

View file

@ -31,7 +31,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if let Some(operator) = typing::to_pep604_operator(value, slice, &checker.semantic) if let Some(operator) = typing::to_pep604_operator(value, slice, &checker.semantic)
{ {
if checker.enabled(Rule::FutureRewritableTypeAnnotation) { if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.is_stub if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py310 && checker.settings.target_version < PythonVersion::Py310
&& checker.settings.target_version >= PythonVersion::Py37 && checker.settings.target_version >= PythonVersion::Py37
&& !checker.semantic.future_annotations() && !checker.semantic.future_annotations()
@ -44,7 +44,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
} }
} }
if checker.enabled(Rule::NonPEP604Annotation) { if checker.enabled(Rule::NonPEP604Annotation) {
if checker.is_stub if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py310 || checker.settings.target_version >= PythonVersion::Py310
|| (checker.settings.target_version >= PythonVersion::Py37 || (checker.settings.target_version >= PythonVersion::Py37
&& checker.semantic.future_annotations() && checker.semantic.future_annotations()
@ -59,7 +59,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
// Ex) list[...] // Ex) list[...]
if checker.enabled(Rule::FutureRequiredTypeAnnotation) { if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
if !checker.is_stub if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py39 && checker.settings.target_version < PythonVersion::Py39
&& !checker.semantic.future_annotations() && !checker.semantic.future_annotations()
&& checker.semantic.in_annotation() && checker.semantic.in_annotation()
@ -176,7 +176,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
typing::to_pep585_generic(expr, &checker.semantic) typing::to_pep585_generic(expr, &checker.semantic)
{ {
if checker.enabled(Rule::FutureRewritableTypeAnnotation) { if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.is_stub if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py39 && checker.settings.target_version < PythonVersion::Py39
&& checker.settings.target_version >= PythonVersion::Py37 && checker.settings.target_version >= PythonVersion::Py37
&& !checker.semantic.future_annotations() && !checker.semantic.future_annotations()
@ -187,7 +187,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
} }
} }
if checker.enabled(Rule::NonPEP585Annotation) { if checker.enabled(Rule::NonPEP585Annotation) {
if checker.is_stub if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py39 || checker.settings.target_version >= PythonVersion::Py39
|| (checker.settings.target_version >= PythonVersion::Py37 || (checker.settings.target_version >= PythonVersion::Py37
&& checker.semantic.future_annotations() && checker.semantic.future_annotations()
@ -272,7 +272,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
]) { ]) {
if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) { if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) {
if checker.enabled(Rule::FutureRewritableTypeAnnotation) { if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.is_stub if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py39 && checker.settings.target_version < PythonVersion::Py39
&& checker.settings.target_version >= PythonVersion::Py37 && checker.settings.target_version >= PythonVersion::Py37
&& !checker.semantic.future_annotations() && !checker.semantic.future_annotations()
@ -285,7 +285,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
} }
} }
if checker.enabled(Rule::NonPEP585Annotation) { if checker.enabled(Rule::NonPEP585Annotation) {
if checker.is_stub if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py39 || checker.settings.target_version >= PythonVersion::Py39
|| (checker.settings.target_version >= PythonVersion::Py37 || (checker.settings.target_version >= PythonVersion::Py37
&& checker.semantic.future_annotations() && checker.semantic.future_annotations()
@ -1066,7 +1066,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}) => { }) => {
// Ex) `str | None` // Ex) `str | None`
if checker.enabled(Rule::FutureRequiredTypeAnnotation) { if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
if !checker.is_stub if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py310 && checker.settings.target_version < PythonVersion::Py310
&& !checker.semantic.future_annotations() && !checker.semantic.future_annotations()
&& checker.semantic.in_annotation() && checker.semantic.in_annotation()
@ -1212,7 +1212,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
kind: _, kind: _,
range: _, range: _,
}) => { }) => {
if checker.is_stub && checker.enabled(Rule::NumericLiteralTooLong) { if checker.source_type.is_stub() && checker.enabled(Rule::NumericLiteralTooLong) {
flake8_pyi::rules::numeric_literal_too_long(checker, expr); flake8_pyi::rules::numeric_literal_too_long(checker, expr);
} }
} }
@ -1221,7 +1221,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
kind: _, kind: _,
range: _, range: _,
}) => { }) => {
if checker.is_stub && checker.enabled(Rule::StringOrBytesTooLong) { if checker.source_type.is_stub() && checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, expr); flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
} }
} }
@ -1249,7 +1249,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnicodeKindPrefix) { if checker.enabled(Rule::UnicodeKindPrefix) {
pyupgrade::rules::unicode_kind_prefix(checker, expr, kind.as_deref()); pyupgrade::rules::unicode_kind_prefix(checker, expr, kind.as_deref());
} }
if checker.is_stub { if checker.source_type.is_stub() {
if checker.enabled(Rule::StringOrBytesTooLong) { if checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, expr); flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
} }

View file

@ -15,7 +15,7 @@ pub(crate) fn parameters(parameters: &Parameters, checker: &mut Checker) {
if checker.settings.rules.enabled(Rule::ImplicitOptional) { if checker.settings.rules.enabled(Rule::ImplicitOptional) {
ruff::rules::implicit_optional(checker, parameters); ruff::rules::implicit_optional(checker, parameters);
} }
if checker.is_stub { if checker.source_type.is_stub() {
if checker.enabled(Rule::TypedArgumentDefaultInStub) { if checker.enabled(Rule::TypedArgumentDefaultInStub) {
flake8_pyi::rules::typed_argument_simple_defaults(checker, parameters); flake8_pyi::rules::typed_argument_simple_defaults(checker, parameters);
} }

View file

@ -133,7 +133,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
} }
if checker.is_stub { if checker.source_type.is_stub() {
if checker.enabled(Rule::PassStatementStubBody) { if checker.enabled(Rule::PassStatementStubBody) {
flake8_pyi::rules::pass_statement_stub_body(checker, body); flake8_pyi::rules::pass_statement_stub_body(checker, body);
} }
@ -168,12 +168,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
type_params.as_ref(), type_params.as_ref(),
); );
} }
if checker.is_stub { if checker.source_type.is_stub() {
if checker.enabled(Rule::StrOrReprDefinedInStub) { if checker.enabled(Rule::StrOrReprDefinedInStub) {
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt); flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
} }
} }
if checker.is_stub || checker.settings.target_version >= PythonVersion::Py311 { if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py311
{
if checker.enabled(Rule::NoReturnArgumentAnnotationInStub) { if checker.enabled(Rule::NoReturnArgumentAnnotationInStub) {
flake8_pyi::rules::no_return_argument_annotation(checker, parameters); flake8_pyi::rules::no_return_argument_annotation(checker, parameters);
} }
@ -412,7 +414,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
body, body,
); );
} }
if !checker.is_stub { if !checker.source_type.is_stub() {
if checker.enabled(Rule::DjangoModelWithoutDunderStr) { if checker.enabled(Rule::DjangoModelWithoutDunderStr) {
flake8_django::rules::model_without_dunder_str(checker, class_def); flake8_django::rules::model_without_dunder_str(checker, class_def);
} }
@ -453,7 +455,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
} }
if !checker.is_stub { if !checker.source_type.is_stub() {
if checker.any_enabled(&[ if checker.any_enabled(&[
Rule::AbstractBaseClassWithoutAbstractMethod, Rule::AbstractBaseClassWithoutAbstractMethod,
Rule::EmptyMethodWithoutAbstractDecorator, Rule::EmptyMethodWithoutAbstractDecorator,
@ -467,7 +469,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
); );
} }
} }
if checker.is_stub { if checker.source_type.is_stub() {
if checker.enabled(Rule::PassStatementStubBody) { if checker.enabled(Rule::PassStatementStubBody) {
flake8_pyi::rules::pass_statement_stub_body(checker, body); flake8_pyi::rules::pass_statement_stub_body(checker, body);
} }
@ -569,7 +571,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
alias, alias,
); );
} }
if !checker.is_stub { if !checker.source_type.is_stub() {
if checker.enabled(Rule::UselessImportAlias) { if checker.enabled(Rule::UselessImportAlias) {
pylint::rules::useless_import_alias(checker, alias); pylint::rules::useless_import_alias(checker, alias);
} }
@ -744,7 +746,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
} }
if checker.is_stub { if checker.source_type.is_stub() {
if checker.enabled(Rule::FutureAnnotationsInStub) { if checker.enabled(Rule::FutureAnnotationsInStub) {
flake8_pyi::rules::from_future_import(checker, import_from); flake8_pyi::rules::from_future_import(checker, import_from);
} }
@ -889,7 +891,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
} }
if !checker.is_stub { if !checker.source_type.is_stub() {
if checker.enabled(Rule::UselessImportAlias) { if checker.enabled(Rule::UselessImportAlias) {
pylint::rules::useless_import_alias(checker, alias); pylint::rules::useless_import_alias(checker, alias);
} }
@ -1013,7 +1015,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
} }
if checker.is_stub { if checker.source_type.is_stub() {
if checker.any_enabled(&[ if checker.any_enabled(&[
Rule::UnrecognizedVersionInfoCheck, Rule::UnrecognizedVersionInfoCheck,
Rule::PatchVersionComparison, Rule::PatchVersionComparison,
@ -1325,7 +1327,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.settings.rules.enabled(Rule::TypeBivariance) { if checker.settings.rules.enabled(Rule::TypeBivariance) {
pylint::rules::type_bivariance(checker, value); pylint::rules::type_bivariance(checker, value);
} }
if checker.is_stub { if checker.source_type.is_stub() {
if checker.any_enabled(&[ if checker.any_enabled(&[
Rule::UnprefixedTypeParam, Rule::UnprefixedTypeParam,
Rule::AssignmentDefaultInStub, Rule::AssignmentDefaultInStub,
@ -1395,7 +1397,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::NonPEP695TypeAlias) { if checker.enabled(Rule::NonPEP695TypeAlias) {
pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt); pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt);
} }
if checker.is_stub { if checker.source_type.is_stub() {
if let Some(value) = value { if let Some(value) = value {
if checker.enabled(Rule::AssignmentDefaultInStub) { if checker.enabled(Rule::AssignmentDefaultInStub) {
// Ignore assignments in function bodies; those are covered by other rules. // Ignore assignments in function bodies; those are covered by other rules.

View file

@ -43,7 +43,7 @@ use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path};
use ruff_python_ast::identifier::Identifier; use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::str::trailing_quote; use ruff_python_ast::str::trailing_quote;
use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor}; use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
use ruff_python_ast::{helpers, str, visitor}; use ruff_python_ast::{helpers, str, visitor, PySourceType};
use ruff_python_codegen::{Generator, Quote, Stylist}; use ruff_python_codegen::{Generator, Quote, Stylist};
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind}; use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
@ -53,7 +53,6 @@ use ruff_python_semantic::{
ModuleKind, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport, ModuleKind, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport,
}; };
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS}; use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_source_file::Locator; use ruff_source_file::Locator;
use crate::checkers::ast::deferred::Deferred; use crate::checkers::ast::deferred::Deferred;
@ -75,8 +74,8 @@ pub(crate) struct Checker<'a> {
package: Option<&'a Path>, package: Option<&'a Path>,
/// The module representation of the current file (e.g., `foo.bar`). /// The module representation of the current file (e.g., `foo.bar`).
module_path: Option<&'a [String]>, module_path: Option<&'a [String]>,
/// Whether the current file is a stub (`.pyi`) file. /// The [`PySourceType`] of the current file.
is_stub: bool, pub(crate) source_type: PySourceType,
/// The [`flags::Noqa`] for the current analysis (i.e., whether to respect suppression /// The [`flags::Noqa`] for the current analysis (i.e., whether to respect suppression
/// comments). /// comments).
noqa: flags::Noqa, noqa: flags::Noqa,
@ -118,6 +117,7 @@ impl<'a> Checker<'a> {
stylist: &'a Stylist, stylist: &'a Stylist,
indexer: &'a Indexer, indexer: &'a Indexer,
importer: Importer<'a>, importer: Importer<'a>,
source_type: PySourceType,
) -> Checker<'a> { ) -> Checker<'a> {
Checker { Checker {
settings, settings,
@ -126,7 +126,7 @@ impl<'a> Checker<'a> {
path, path,
package, package,
module_path: module.path(), module_path: module.path(),
is_stub: is_python_stub_file(path), source_type,
locator, locator,
stylist, stylist,
indexer, indexer,
@ -233,11 +233,6 @@ impl<'a> Checker<'a> {
&self.semantic &self.semantic
} }
/// Return `true` if the current file is a stub file (`.pyi`).
pub(crate) const fn is_stub(&self) -> bool {
self.is_stub
}
/// The [`Path`] to the file under analysis. /// The [`Path`] to the file under analysis.
pub(crate) const fn path(&self) -> &'a Path { pub(crate) const fn path(&self) -> &'a Path {
self.path self.path
@ -1786,7 +1781,7 @@ impl<'a> Checker<'a> {
pyupgrade::rules::quoted_annotation(self, value, range); pyupgrade::rules::quoted_annotation(self, value, range);
} }
} }
if self.is_stub { if self.source_type.is_stub() {
if self.enabled(Rule::QuotedAnnotationInStub) { if self.enabled(Rule::QuotedAnnotationInStub) {
flake8_pyi::rules::quoted_annotation_in_stub(self, value, range); flake8_pyi::rules::quoted_annotation_in_stub(self, value, range);
} }
@ -1928,6 +1923,7 @@ pub(crate) fn check_ast(
noqa: flags::Noqa, noqa: flags::Noqa,
path: &Path, path: &Path,
package: Option<&Path>, package: Option<&Path>,
source_type: PySourceType,
) -> Vec<Diagnostic> { ) -> Vec<Diagnostic> {
let module_path = package.and_then(|package| to_module_path(package, path)); let module_path = package.and_then(|package| to_module_path(package, path));
let module = Module { let module = Module {
@ -1955,6 +1951,7 @@ pub(crate) fn check_ast(
stylist, stylist,
indexer, indexer,
Importer::new(python_ast, locator, stylist), Importer::new(python_ast, locator, stylist),
source_type,
); );
checker.bind_builtins(); checker.bind_builtins();

View file

@ -2,7 +2,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::path::Path; use std::path::Path;
use ruff_python_ast::{self as ast, Ranged, Stmt, Suite}; use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt, Suite};
use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Diagnostic;
use ruff_python_ast::helpers::to_module_path; use ruff_python_ast::helpers::to_module_path;
@ -10,7 +10,7 @@ use ruff_python_ast::imports::{ImportMap, ModuleImport};
use ruff_python_ast::statement_visitor::StatementVisitor; use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_source_file::Locator; use ruff_source_file::Locator;
use crate::directives::IsortDirectives; use crate::directives::IsortDirectives;
@ -87,12 +87,12 @@ pub(crate) fn check_imports(
path: &Path, path: &Path,
package: Option<&Path>, package: Option<&Path>,
source_kind: Option<&SourceKind>, source_kind: Option<&SourceKind>,
source_type: PySourceType,
) -> (Vec<Diagnostic>, Option<ImportMap>) { ) -> (Vec<Diagnostic>, Option<ImportMap>) {
let is_stub = is_python_stub_file(path);
// Extract all import blocks from the AST. // Extract all import blocks from the AST.
let tracker = { let tracker = {
let mut tracker = BlockBuilder::new(locator, directives, is_stub, source_kind); let mut tracker =
BlockBuilder::new(locator, directives, source_type.is_stub(), source_kind);
tracker.visit_body(python_ast); tracker.visit_body(python_ast);
tracker tracker
}; };
@ -104,7 +104,13 @@ pub(crate) fn check_imports(
for block in &blocks { for block in &blocks {
if !block.imports.is_empty() { if !block.imports.is_empty() {
if let Some(diagnostic) = isort::rules::organize_imports( if let Some(diagnostic) = isort::rules::organize_imports(
block, locator, stylist, indexer, settings, package, block,
locator,
stylist,
indexer,
settings,
package,
source_type,
) { ) {
diagnostics.push(diagnostic); diagnostics.push(diagnostic);
} }
@ -113,7 +119,11 @@ pub(crate) fn check_imports(
} }
if settings.rules.enabled(Rule::MissingRequiredImport) { if settings.rules.enabled(Rule::MissingRequiredImport) {
diagnostics.extend(isort::rules::add_required_imports( diagnostics.extend(isort::rules::add_required_imports(
python_ast, locator, stylist, settings, is_stub, python_ast,
locator,
stylist,
settings,
source_type,
)); ));
} }

View file

@ -1,8 +1,8 @@
//! Insert statements into Python code. //! Insert statements into Python code.
use std::ops::Add; use std::ops::Add;
use ruff_python_ast::{Ranged, Stmt}; use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_python_parser::{lexer, Mode, Tok}; use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_text_size::TextSize; use ruff_text_size::TextSize;
use ruff_diagnostics::Edit; use ruff_diagnostics::Edit;
@ -137,6 +137,7 @@ impl<'a> Insertion<'a> {
mut location: TextSize, mut location: TextSize,
locator: &Locator<'a>, locator: &Locator<'a>,
stylist: &Stylist, stylist: &Stylist,
source_type: PySourceType,
) -> Insertion<'a> { ) -> Insertion<'a> {
enum Awaiting { enum Awaiting {
Colon(u32), Colon(u32),
@ -146,7 +147,7 @@ impl<'a> Insertion<'a> {
let mut state = Awaiting::Colon(0); let mut state = Awaiting::Colon(0);
for (tok, range) in for (tok, range) in
lexer::lex_starts_at(locator.after(location), Mode::Module, location).flatten() lexer::lex_starts_at(locator.after(location), source_type.as_mode(), location).flatten()
{ {
match state { match state {
// Iterate until we find the colon indicating the start of the block body. // Iterate until we find the colon indicating the start of the block body.
@ -300,12 +301,12 @@ fn match_leading_semicolon(s: &str) -> Option<TextSize> {
mod tests { mod tests {
use anyhow::Result; use anyhow::Result;
use ruff_python_parser::lexer::LexResult; use ruff_python_ast::PySourceType;
use ruff_text_size::TextSize;
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_parser::parse_suite; use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{parse_suite, Mode};
use ruff_source_file::{LineEnding, Locator}; use ruff_source_file::{LineEnding, Locator};
use ruff_text_size::TextSize;
use super::Insertion; use super::Insertion;
@ -313,7 +314,7 @@ mod tests {
fn start_of_file() -> Result<()> { fn start_of_file() -> Result<()> {
fn insert(contents: &str) -> Result<Insertion> { fn insert(contents: &str) -> Result<Insertion> {
let program = parse_suite(contents, "<filename>")?; let program = parse_suite(contents, "<filename>")?;
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents); let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, Mode::Module);
let locator = Locator::new(contents); let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator); let stylist = Stylist::from_tokens(&tokens, &locator);
Ok(Insertion::start_of_file(&program, &locator, &stylist)) Ok(Insertion::start_of_file(&program, &locator, &stylist))
@ -424,10 +425,10 @@ x = 1
#[test] #[test]
fn start_of_block() { fn start_of_block() {
fn insert(contents: &str, offset: TextSize) -> Insertion { fn insert(contents: &str, offset: TextSize) -> Insertion {
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents); let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, Mode::Module);
let locator = Locator::new(contents); let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator); let stylist = Stylist::from_tokens(&tokens, &locator);
Insertion::start_of_block(offset, &locator, &stylist) Insertion::start_of_block(offset, &locator, &stylist, PySourceType::default())
} }
let contents = "if True: pass"; let contents = "if True: pass";

View file

@ -7,7 +7,7 @@ use std::error::Error;
use anyhow::Result; use anyhow::Result;
use libcst_native::{ImportAlias, Name, NameOrAttribute}; use libcst_native::{ImportAlias, Name, NameOrAttribute};
use ruff_python_ast::{self as ast, Ranged, Stmt, Suite}; use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt, Suite};
use ruff_text_size::TextSize; use ruff_text_size::TextSize;
use ruff_diagnostics::Edit; use ruff_diagnostics::Edit;
@ -121,6 +121,7 @@ impl<'a> Importer<'a> {
import: &StmtImports, import: &StmtImports,
at: TextSize, at: TextSize,
semantic: &SemanticModel, semantic: &SemanticModel,
source_type: PySourceType,
) -> Result<TypingImportEdit> { ) -> Result<TypingImportEdit> {
// Generate the modified import statement. // Generate the modified import statement.
let content = autofix::codemods::retain_imports( let content = autofix::codemods::retain_imports(
@ -140,7 +141,7 @@ impl<'a> Importer<'a> {
// Add the import to a `TYPE_CHECKING` block. // Add the import to a `TYPE_CHECKING` block.
let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) { let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) {
// Add the import to the `TYPE_CHECKING` block. // Add the import to the `TYPE_CHECKING` block.
self.add_to_type_checking_block(&content, block.start()) self.add_to_type_checking_block(&content, block.start(), source_type)
} else { } else {
// Add the import to a new `TYPE_CHECKING` block. // Add the import to a new `TYPE_CHECKING` block.
self.add_type_checking_block( self.add_type_checking_block(
@ -353,8 +354,13 @@ impl<'a> Importer<'a> {
} }
/// Add an import statement to an existing `TYPE_CHECKING` block. /// Add an import statement to an existing `TYPE_CHECKING` block.
fn add_to_type_checking_block(&self, content: &str, at: TextSize) -> Edit { fn add_to_type_checking_block(
Insertion::start_of_block(at, self.locator, self.stylist).into_edit(content) &self,
content: &str,
at: TextSize,
source_type: PySourceType,
) -> Edit {
Insertion::start_of_block(at, self.locator, self.stylist, source_type).into_edit(content)
} }
/// Return the import statement that precedes the given position, if any. /// Return the import statement that precedes the given position, if any.

View file

@ -24,8 +24,6 @@ use crate::IOError;
pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb"; pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb";
const MAGIC_PREFIX: [&str; 3] = ["%", "!", "?"];
/// Run round-trip source code generation on a given Jupyter notebook file path. /// Run round-trip source code generation on a given Jupyter notebook file path.
pub fn round_trip(path: &Path) -> anyhow::Result<String> { pub fn round_trip(path: &Path) -> anyhow::Result<String> {
let mut notebook = Notebook::read(path).map_err(|err| { let mut notebook = Notebook::read(path).map_err(|err| {
@ -78,26 +76,21 @@ impl Cell {
/// Return `true` if it's a valid code cell. /// Return `true` if it's a valid code cell.
/// ///
/// A valid code cell is a cell where the cell type is [`Cell::Code`] and the /// A valid code cell is a cell where the cell type is [`Cell::Code`] and the
/// source doesn't contain a magic, shell or help command. /// source doesn't contain a cell magic.
fn is_valid_code_cell(&self) -> bool { fn is_valid_code_cell(&self) -> bool {
let source = match self { let source = match self {
Cell::Code(cell) => &cell.source, Cell::Code(cell) => &cell.source,
_ => return false, _ => return false,
}; };
// Ignore a cell if it contains a magic command. There could be valid // Ignore cells containing cell magic. This is different from line magic
// Python code as well, but we'll ignore that for now. // which is allowed and ignored by the parser.
// TODO(dhruvmanila): https://github.com/psf/black/blob/main/src/black/handle_ipynb_magics.py
!match source { !match source {
SourceValue::String(string) => string.lines().any(|line| { SourceValue::String(string) => string
MAGIC_PREFIX .lines()
.iter() .any(|line| line.trim_start().starts_with("%%")),
.any(|prefix| line.trim_start().starts_with(prefix)) SourceValue::StringArray(string_array) => string_array
}), .iter()
SourceValue::StringArray(string_array) => string_array.iter().any(|line| { .any(|line| line.trim_start().starts_with("%%")),
MAGIC_PREFIX
.iter()
.any(|prefix| line.trim_start().starts_with(prefix))
}),
} }
} }
} }
@ -513,9 +506,10 @@ mod tests {
} }
#[test_case(Path::new("markdown.json"), false; "markdown")] #[test_case(Path::new("markdown.json"), false; "markdown")]
#[test_case(Path::new("only_magic.json"), false; "only_magic")] #[test_case(Path::new("only_magic.json"), true; "only_magic")]
#[test_case(Path::new("code_and_magic.json"), false; "code_and_magic")] #[test_case(Path::new("code_and_magic.json"), true; "code_and_magic")]
#[test_case(Path::new("only_code.json"), true; "only_code")] #[test_case(Path::new("only_code.json"), true; "only_code")]
#[test_case(Path::new("cell_magic.json"), false; "cell_magic")]
fn test_is_valid_code_cell(path: &Path, expected: bool) -> Result<()> { fn test_is_valid_code_cell(path: &Path, expected: bool) -> Result<()> {
assert_eq!(read_jupyter_cell(path)?.is_valid_code_cell(), expected); assert_eq!(read_jupyter_cell(path)?.is_valid_code_cell(), expected);
Ok(()) Ok(())
@ -567,7 +561,7 @@ print("after empty cells")
#[test] #[test]
fn test_import_sorting() -> Result<()> { fn test_import_sorting() -> Result<()> {
let path = "isort.ipynb".to_string(); let path = "isort.ipynb".to_string();
let (diagnostics, source_kind) = test_notebook_path( let (diagnostics, source_kind, _) = test_notebook_path(
&path, &path,
Path::new("isort_expected.ipynb"), Path::new("isort_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnsortedImports), &settings::Settings::for_rule(Rule::UnsortedImports),
@ -576,10 +570,22 @@ print("after empty cells")
Ok(()) Ok(())
} }
#[test]
fn test_line_magics() -> Result<()> {
let path = "line_magics.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("line_magics_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
assert_messages!(diagnostics, path, source_kind);
Ok(())
}
#[test] #[test]
fn test_json_consistency() -> Result<()> { fn test_json_consistency() -> Result<()> {
let path = "before_fix.ipynb".to_string(); let path = "before_fix.ipynb".to_string();
let (_, source_kind) = test_notebook_path( let (_, _, source_kind) = test_notebook_path(
path, path,
Path::new("after_fix.ipynb"), Path::new("after_fix.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport), &settings::Settings::for_rule(Rule::UnusedImport),

View file

@ -47,4 +47,43 @@ isort.ipynb:cell 2:1:1: I001 [*] Import block is un-sorted or un-formatted
7 9 | def foo(): 7 9 | def foo():
8 10 | pass 8 10 | pass
isort.ipynb:cell 3:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from pathlib import Path
2 | | import sys
3 | |
4 | | %matplotlib \
| |_^ I001
5 | --inline
|
= help: Organize imports
Fix
6 6 | # Newline should be added here
7 7 | def foo():
8 8 | pass
9 |+import sys
9 10 | from pathlib import Path
10 |-import sys
11 11 |
12 12 | %matplotlib \
13 13 | --inline
isort.ipynb:cell 3:7:1: I001 [*] Import block is un-sorted or un-formatted
|
5 | --inline
6 |
7 | / import math
8 | | import abc
|
= help: Organize imports
Fix
12 12 | %matplotlib \
13 13 | --inline
14 14 |
15 |+import abc
15 16 | import math
16 |-import abc

View file

@ -0,0 +1,23 @@
---
source: crates/ruff/src/jupyter/notebook.rs
---
line_magics.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
|
3 | %matplotlib inline
4 |
5 | import os
| ^^ F401
6 |
7 | _ = math.pi
|
= help: Remove unused import: `os`
Fix
2 2 |
3 3 | %matplotlib inline
4 4 |
5 |-import os
6 5 |
7 6 | _ = math.pi

View file

@ -7,14 +7,15 @@ use colored::Colorize;
use itertools::Itertools; use itertools::Itertools;
use log::error; use log::error;
use ruff_python_parser::lexer::LexResult; use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::ParseError; use ruff_python_parser::{AsMode, ParseError};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Diagnostic;
use ruff_python_ast::imports::ImportMap; use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_source_file::{Locator, SourceFileBuilder}; use ruff_source_file::{Locator, SourceFileBuilder};
use crate::autofix::{fix_file, FixResult}; use crate::autofix::{fix_file, FixResult};
@ -81,6 +82,7 @@ pub fn check_path(
settings: &Settings, settings: &Settings,
noqa: flags::Noqa, noqa: flags::Noqa,
source_kind: Option<&SourceKind>, source_kind: Option<&SourceKind>,
source_type: PySourceType,
) -> LinterResult<(Vec<Diagnostic>, Option<ImportMap>)> { ) -> LinterResult<(Vec<Diagnostic>, Option<ImportMap>)> {
// Aggregate all diagnostics. // Aggregate all diagnostics.
let mut diagnostics = vec![]; let mut diagnostics = vec![];
@ -101,9 +103,13 @@ pub fn check_path(
.iter_enabled() .iter_enabled()
.any(|rule_code| rule_code.lint_source().is_tokens()) .any(|rule_code| rule_code.lint_source().is_tokens())
{ {
let is_stub = is_python_stub_file(path);
diagnostics.extend(check_tokens( diagnostics.extend(check_tokens(
&tokens, path, locator, indexer, settings, is_stub, &tokens,
path,
locator,
indexer,
settings,
source_type.is_stub(),
)); ));
} }
@ -138,7 +144,11 @@ pub fn check_path(
.iter_enabled() .iter_enabled()
.any(|rule_code| rule_code.lint_source().is_imports()); .any(|rule_code| rule_code.lint_source().is_imports());
if use_ast || use_imports || use_doc_lines { if use_ast || use_imports || use_doc_lines {
match ruff_python_parser::parse_program_tokens(tokens, &path.to_string_lossy()) { match ruff_python_parser::parse_program_tokens(
tokens,
&path.to_string_lossy(),
source_type.is_jupyter(),
) {
Ok(python_ast) => { Ok(python_ast) => {
if use_ast { if use_ast {
diagnostics.extend(check_ast( diagnostics.extend(check_ast(
@ -151,6 +161,7 @@ pub fn check_path(
noqa, noqa,
path, path,
package, package,
source_type,
)); ));
} }
if use_imports { if use_imports {
@ -164,6 +175,7 @@ pub fn check_path(
path, path,
package, package,
source_kind, source_kind,
source_type,
); );
imports = module_imports; imports = module_imports;
diagnostics.extend(import_diagnostics); diagnostics.extend(import_diagnostics);
@ -256,11 +268,13 @@ const MAX_ITERATIONS: usize = 100;
/// Add any missing `# noqa` pragmas to the source code at the given `Path`. /// Add any missing `# noqa` pragmas to the source code at the given `Path`.
pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings) -> Result<usize> { pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings) -> Result<usize> {
let source_type = PySourceType::from(path);
// Read the file from disk. // Read the file from disk.
let contents = std::fs::read_to_string(path)?; let contents = std::fs::read_to_string(path)?;
// Tokenize once. // Tokenize once.
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents); let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents, source_type.as_mode());
// Map row and column locations to byte slices (lazily). // Map row and column locations to byte slices (lazily).
let locator = Locator::new(&contents); let locator = Locator::new(&contents);
@ -294,6 +308,7 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings
settings, settings,
flags::Noqa::Disabled, flags::Noqa::Disabled,
None, None,
source_type,
); );
// Log any parse errors. // Log any parse errors.
@ -326,9 +341,10 @@ pub fn lint_only(
settings: &Settings, settings: &Settings,
noqa: flags::Noqa, noqa: flags::Noqa,
source_kind: Option<&SourceKind>, source_kind: Option<&SourceKind>,
source_type: PySourceType,
) -> LinterResult<(Vec<Message>, Option<ImportMap>)> { ) -> LinterResult<(Vec<Message>, Option<ImportMap>)> {
// Tokenize once. // Tokenize once.
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents); let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, source_type.as_mode());
// Map row and column locations to byte slices (lazily). // Map row and column locations to byte slices (lazily).
let locator = Locator::new(contents); let locator = Locator::new(contents);
@ -359,6 +375,7 @@ pub fn lint_only(
settings, settings,
noqa, noqa,
source_kind, source_kind,
source_type,
); );
result.map(|(diagnostics, imports)| { result.map(|(diagnostics, imports)| {
@ -405,6 +422,7 @@ pub fn lint_fix<'a>(
noqa: flags::Noqa, noqa: flags::Noqa,
settings: &Settings, settings: &Settings,
source_kind: &mut SourceKind, source_kind: &mut SourceKind,
source_type: PySourceType,
) -> Result<FixerResult<'a>> { ) -> Result<FixerResult<'a>> {
let mut transformed = Cow::Borrowed(contents); let mut transformed = Cow::Borrowed(contents);
@ -420,7 +438,8 @@ pub fn lint_fix<'a>(
// Continuously autofix until the source code stabilizes. // Continuously autofix until the source code stabilizes.
loop { loop {
// Tokenize once. // Tokenize once.
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&transformed); let tokens: Vec<LexResult> =
ruff_python_parser::tokenize(&transformed, source_type.as_mode());
// Map row and column locations to byte slices (lazily). // Map row and column locations to byte slices (lazily).
let locator = Locator::new(&transformed); let locator = Locator::new(&transformed);
@ -451,6 +470,7 @@ pub fn lint_fix<'a>(
settings, settings,
noqa, noqa,
Some(source_kind), Some(source_kind),
source_type,
); );
if iterations == 0 { if iterations == 0 {

View file

@ -1,6 +1,6 @@
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use ruff_python_ast::{Ranged, Stmt}; use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_python_parser::{lexer, Mode, Tok}; use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_diagnostics::Edit; use ruff_diagnostics::Edit;
use ruff_source_file::Locator; use ruff_source_file::Locator;
@ -10,6 +10,7 @@ pub(crate) fn add_return_annotation(
locator: &Locator, locator: &Locator,
stmt: &Stmt, stmt: &Stmt,
annotation: &str, annotation: &str,
source_type: PySourceType,
) -> Result<Edit> { ) -> Result<Edit> {
let contents = &locator.contents()[stmt.range()]; let contents = &locator.contents()[stmt.range()];
@ -17,7 +18,9 @@ pub(crate) fn add_return_annotation(
let mut seen_lpar = false; let mut seen_lpar = false;
let mut seen_rpar = false; let mut seen_rpar = false;
let mut count = 0u32; let mut count = 0u32;
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, stmt.start()).flatten() { for (tok, range) in
lexer::lex_starts_at(contents, source_type.as_mode(), stmt.start()).flatten()
{
if seen_lpar && seen_rpar { if seen_lpar && seen_rpar {
if matches!(tok, Tok::Colon) { if matches!(tok, Tok::Colon) {
return Ok(Edit::insertion(format!(" -> {annotation}"), range.start())); return Ok(Edit::insertion(format!(" -> {annotation}"), range.start()));

View file

@ -709,8 +709,13 @@ pub(crate) fn definition(
); );
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
fixes::add_return_annotation(checker.locator(), stmt, "None") fixes::add_return_annotation(
.map(Fix::suggested) checker.locator(),
stmt,
"None",
checker.source_type,
)
.map(Fix::suggested)
}); });
} }
diagnostics.push(diagnostic); diagnostics.push(diagnostic);
@ -727,8 +732,13 @@ pub(crate) fn definition(
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
if let Some(return_type) = simple_magic_return_type(name) { if let Some(return_type) = simple_magic_return_type(name) {
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
fixes::add_return_annotation(checker.locator(), stmt, return_type) fixes::add_return_annotation(
.map(Fix::suggested) checker.locator(),
stmt,
return_type,
checker.source_type,
)
.map(Fix::suggested)
}); });
} }
} }

View file

@ -550,6 +550,7 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D
arguments, arguments,
edits::Parentheses::Preserve, edits::Parentheses::Preserve,
checker.locator(), checker.locator(),
checker.source_type,
) )
.map(Fix::suggested) .map(Fix::suggested)
}); });

View file

@ -1,5 +1,7 @@
use ruff_python_ast::{self as ast, Arguments, Constant, Decorator, Expr, ExprContext, Ranged}; use ruff_python_ast::{
use ruff_python_parser::{lexer, Mode, Tok}; self as ast, Arguments, Constant, Decorator, Expr, ExprContext, PySourceType, Ranged,
};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
@ -95,7 +97,12 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
/// ``` /// ```
/// ///
/// This method assumes that the first argument is a string. /// This method assumes that the first argument is a string.
fn get_parametrize_name_range(decorator: &Decorator, expr: &Expr, locator: &Locator) -> TextRange { fn get_parametrize_name_range(
decorator: &Decorator,
expr: &Expr,
locator: &Locator,
source_type: PySourceType,
) -> TextRange {
let mut locations = Vec::new(); let mut locations = Vec::new();
let mut implicit_concat = None; let mut implicit_concat = None;
@ -103,7 +110,7 @@ fn get_parametrize_name_range(decorator: &Decorator, expr: &Expr, locator: &Loca
// decorator to find them. // decorator to find them.
for (tok, range) in lexer::lex_starts_at( for (tok, range) in lexer::lex_starts_at(
locator.slice(decorator.range()), locator.slice(decorator.range()),
Mode::Module, source_type.as_mode(),
decorator.start(), decorator.start(),
) )
.flatten() .flatten()
@ -141,8 +148,12 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
if names.len() > 1 { if names.len() > 1 {
match names_type { match names_type {
types::ParametrizeNameType::Tuple => { types::ParametrizeNameType::Tuple => {
let name_range = let name_range = get_parametrize_name_range(
get_parametrize_name_range(decorator, expr, checker.locator()); decorator,
expr,
checker.locator(),
checker.source_type,
);
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
PytestParametrizeNamesWrongType { PytestParametrizeNamesWrongType {
expected: names_type, expected: names_type,
@ -172,8 +183,12 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
types::ParametrizeNameType::List => { types::ParametrizeNameType::List => {
let name_range = let name_range = get_parametrize_name_range(
get_parametrize_name_range(decorator, expr, checker.locator()); decorator,
expr,
checker.locator(),
checker.source_type,
);
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
PytestParametrizeNamesWrongType { PytestParametrizeNamesWrongType {
expected: names_type, expected: names_type,

View file

@ -1,5 +1,5 @@
use ruff_python_ast::{self as ast, Arguments, Expr, Ranged}; use ruff_python_ast::{self as ast, Arguments, Expr, PySourceType, Ranged};
use ruff_python_parser::{lexer, Mode, Tok}; use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
@ -71,7 +71,7 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
return; return;
} }
let range = match_parens(func.end(), checker.locator()) let range = match_parens(func.end(), checker.locator(), checker.source_type)
.expect("Expected call to include parentheses"); .expect("Expected call to include parentheses");
let mut diagnostic = Diagnostic::new(UnnecessaryParenOnRaiseException, range); let mut diagnostic = Diagnostic::new(UnnecessaryParenOnRaiseException, range);
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
@ -82,14 +82,18 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
} }
/// Return the range of the first parenthesis pair after a given [`TextSize`]. /// Return the range of the first parenthesis pair after a given [`TextSize`].
fn match_parens(start: TextSize, locator: &Locator) -> Option<TextRange> { fn match_parens(
start: TextSize,
locator: &Locator,
source_type: PySourceType,
) -> Option<TextRange> {
let contents = &locator.contents()[usize::from(start)..]; let contents = &locator.contents()[usize::from(start)..];
let mut fix_start = None; let mut fix_start = None;
let mut fix_end = None; let mut fix_end = None;
let mut count = 0u32; let mut count = 0u32;
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, start).flatten() { for (tok, range) in lexer::lex_starts_at(contents, source_type.as_mode(), start).flatten() {
match tok { match tok {
Tok::Lpar => { Tok::Lpar => {
if count == 0 { if count == 0 {

View file

@ -378,6 +378,7 @@ pub(crate) fn nested_if_statements(
let colon = first_colon_range( let colon = first_colon_range(
TextRange::new(test.end(), first_stmt.start()), TextRange::new(test.end(), first_stmt.start()),
checker.locator().contents(), checker.locator().contents(),
checker.source_type.is_jupyter(),
); );
// Check if the parent is already emitting a larger diagnostic including this if statement // Check if the parent is already emitting a larger diagnostic including this if statement

View file

@ -119,6 +119,7 @@ pub(crate) fn multiple_with_statements(
body.first().expect("Expected body to be non-empty").start(), body.first().expect("Expected body to be non-empty").start(),
), ),
checker.locator().contents(), checker.locator().contents(),
checker.source_type.is_jupyter(),
); );
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(

View file

@ -447,6 +447,7 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result
}, },
at, at,
checker.semantic(), checker.semantic(),
checker.source_type,
)?; )?;
Ok( Ok(

View file

@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Ranged, Stmt}; use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt};
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
use ruff_source_file::Locator; use ruff_source_file::Locator;
@ -13,6 +13,7 @@ pub(crate) fn annotate_imports<'a>(
comments: Vec<Comment<'a>>, comments: Vec<Comment<'a>>,
locator: &Locator, locator: &Locator,
split_on_trailing_comma: bool, split_on_trailing_comma: bool,
source_type: PySourceType,
) -> Vec<AnnotatedImport<'a>> { ) -> Vec<AnnotatedImport<'a>> {
let mut comments_iter = comments.into_iter().peekable(); let mut comments_iter = comments.into_iter().peekable();
@ -119,7 +120,7 @@ pub(crate) fn annotate_imports<'a>(
names: aliases, names: aliases,
level: level.map(|level| level.to_u32()), level: level.map(|level| level.to_u32()),
trailing_comma: if split_on_trailing_comma { trailing_comma: if split_on_trailing_comma {
trailing_comma(import, locator) trailing_comma(import, locator, source_type)
} else { } else {
TrailingComma::default() TrailingComma::default()
}, },

View file

@ -1,6 +1,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use ruff_python_parser::{lexer, Mode, Tok}; use ruff_python_ast::PySourceType;
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use ruff_source_file::Locator; use ruff_source_file::Locator;
@ -22,9 +23,13 @@ impl Comment<'_> {
} }
/// Collect all comments in an import block. /// Collect all comments in an import block.
pub(crate) fn collect_comments<'a>(range: TextRange, locator: &'a Locator) -> Vec<Comment<'a>> { pub(crate) fn collect_comments<'a>(
range: TextRange,
locator: &'a Locator,
source_type: PySourceType,
) -> Vec<Comment<'a>> {
let contents = locator.slice(range); let contents = locator.slice(range);
lexer::lex_starts_at(contents, Mode::Module, range.start()) lexer::lex_starts_at(contents, source_type.as_mode(), range.start())
.flatten() .flatten()
.filter_map(|(tok, range)| { .filter_map(|(tok, range)| {
if let Tok::Comment(value) = tok { if let Tok::Comment(value) = tok {

View file

@ -1,5 +1,5 @@
use ruff_python_ast::{Ranged, Stmt}; use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_python_parser::{lexer, Mode, Tok}; use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_python_trivia::PythonWhitespace; use ruff_python_trivia::PythonWhitespace;
use ruff_source_file::{Locator, UniversalNewlines}; use ruff_source_file::{Locator, UniversalNewlines};
@ -8,11 +8,15 @@ use crate::rules::isort::types::TrailingComma;
/// Return `true` if a `Stmt::ImportFrom` statement ends with a magic /// Return `true` if a `Stmt::ImportFrom` statement ends with a magic
/// trailing comma. /// trailing comma.
pub(super) fn trailing_comma(stmt: &Stmt, locator: &Locator) -> TrailingComma { pub(super) fn trailing_comma(
stmt: &Stmt,
locator: &Locator,
source_type: PySourceType,
) -> TrailingComma {
let contents = locator.slice(stmt.range()); let contents = locator.slice(stmt.range());
let mut count = 0u32; let mut count = 0u32;
let mut trailing_comma = TrailingComma::Absent; let mut trailing_comma = TrailingComma::Absent;
for (tok, _) in lexer::lex_starts_at(contents, Mode::Module, stmt.start()).flatten() { for (tok, _) in lexer::lex_starts_at(contents, source_type.as_mode(), stmt.start()).flatten() {
if matches!(tok, Tok::Lpar) { if matches!(tok, Tok::Lpar) {
count = count.saturating_add(1); count = count.saturating_add(1);
} }

View file

@ -11,6 +11,7 @@ pub use categorize::{ImportSection, ImportType};
use comments::Comment; use comments::Comment;
use normalize::normalize_imports; use normalize::normalize_imports;
use order::order_imports; use order::order_imports;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_source_file::Locator; use ruff_source_file::Locator;
use settings::RelativeImportsOrder; use settings::RelativeImportsOrder;
@ -72,6 +73,7 @@ pub(crate) fn format_imports(
stylist: &Stylist, stylist: &Stylist,
src: &[PathBuf], src: &[PathBuf],
package: Option<&Path>, package: Option<&Path>,
source_type: PySourceType,
combine_as_imports: bool, combine_as_imports: bool,
force_single_line: bool, force_single_line: bool,
force_sort_within_sections: bool, force_sort_within_sections: bool,
@ -94,7 +96,13 @@ pub(crate) fn format_imports(
section_order: &[ImportSection], section_order: &[ImportSection],
) -> String { ) -> String {
let trailer = &block.trailer; let trailer = &block.trailer;
let block = annotate_imports(&block.imports, comments, locator, split_on_trailing_comma); let block = annotate_imports(
&block.imports,
comments,
locator,
split_on_trailing_comma,
source_type,
);
// Normalize imports (i.e., deduplicate, aggregate `from` imports). // Normalize imports (i.e., deduplicate, aggregate `from` imports).
let block = normalize_imports( let block = normalize_imports(

View file

@ -1,5 +1,5 @@
use log::error; use log::error;
use ruff_python_ast::{self as ast, Stmt, Suite}; use ruff_python_ast::{self as ast, PySourceType, Stmt, Suite};
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
@ -91,7 +91,7 @@ fn add_required_import(
locator: &Locator, locator: &Locator,
stylist: &Stylist, stylist: &Stylist,
settings: &Settings, settings: &Settings,
is_stub: bool, source_type: PySourceType,
) -> Option<Diagnostic> { ) -> Option<Diagnostic> {
// Don't add imports to semantically-empty files. // Don't add imports to semantically-empty files.
if python_ast.iter().all(is_docstring_stmt) { if python_ast.iter().all(is_docstring_stmt) {
@ -99,7 +99,7 @@ fn add_required_import(
} }
// We don't need to add `__future__` imports to stubs. // We don't need to add `__future__` imports to stubs.
if is_stub && required_import.is_future_import() { if source_type.is_stub() && required_import.is_future_import() {
return None; return None;
} }
@ -131,7 +131,7 @@ pub(crate) fn add_required_imports(
locator: &Locator, locator: &Locator,
stylist: &Stylist, stylist: &Stylist,
settings: &Settings, settings: &Settings,
is_stub: bool, source_type: PySourceType,
) -> Vec<Diagnostic> { ) -> Vec<Diagnostic> {
settings settings
.isort .isort
@ -172,7 +172,7 @@ pub(crate) fn add_required_imports(
locator, locator,
stylist, stylist,
settings, settings,
is_stub, source_type,
) )
}) })
.collect(), .collect(),
@ -190,7 +190,7 @@ pub(crate) fn add_required_imports(
locator, locator,
stylist, stylist,
settings, settings,
is_stub, source_type,
) )
}) })
.collect(), .collect(),

View file

@ -1,7 +1,7 @@
use std::path::Path; use std::path::Path;
use itertools::{EitherOrBoth, Itertools}; use itertools::{EitherOrBoth, Itertools};
use ruff_python_ast::{Ranged, Stmt}; use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
@ -87,6 +87,7 @@ pub(crate) fn organize_imports(
indexer: &Indexer, indexer: &Indexer,
settings: &Settings, settings: &Settings,
package: Option<&Path>, package: Option<&Path>,
source_type: PySourceType,
) -> Option<Diagnostic> { ) -> Option<Diagnostic> {
let indentation = locator.slice(extract_indentation_range(&block.imports, locator)); let indentation = locator.slice(extract_indentation_range(&block.imports, locator));
let indentation = leading_indentation(indentation); let indentation = leading_indentation(indentation);
@ -105,6 +106,7 @@ pub(crate) fn organize_imports(
let comments = comments::collect_comments( let comments = comments::collect_comments(
TextRange::new(range.start(), locator.full_line_end(range.end())), TextRange::new(range.start(), locator.full_line_end(range.end())),
locator, locator,
source_type,
); );
let trailing_line_end = if block.trailer.is_none() { let trailing_line_end = if block.trailer.is_none() {
@ -123,6 +125,7 @@ pub(crate) fn organize_imports(
stylist, stylist,
&settings.src, &settings.src,
package, package,
source_type,
settings.isort.combine_as_imports, settings.isort.combine_as_imports,
settings.isort.force_single_line, settings.isort.force_single_line,
settings.isort.force_sort_within_sections, settings.isort.force_sort_within_sections,

View file

@ -1,7 +1,7 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_true; use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::{self as ast, Keyword, Ranged}; use ruff_python_ast::{self as ast, Keyword, PySourceType, Ranged};
use ruff_python_semantic::{BindingKind, Import}; use ruff_python_semantic::{BindingKind, Import};
use ruff_source_file::Locator; use ruff_source_file::Locator;
@ -93,9 +93,12 @@ pub(crate) fn inplace_argument(checker: &mut Checker, call: &ast::ExprCall) {
&& checker.semantic().expr_parent().is_none() && checker.semantic().expr_parent().is_none()
&& !checker.semantic().scope().kind.is_lambda() && !checker.semantic().scope().kind.is_lambda()
{ {
if let Some(fix) = if let Some(fix) = convert_inplace_argument_to_assignment(
convert_inplace_argument_to_assignment(checker.locator(), call, keyword) checker.locator(),
{ call,
keyword,
checker.source_type,
) {
diagnostic.set_fix(fix); diagnostic.set_fix(fix);
} }
} }
@ -116,6 +119,7 @@ fn convert_inplace_argument_to_assignment(
locator: &Locator, locator: &Locator,
call: &ast::ExprCall, call: &ast::ExprCall,
keyword: &Keyword, keyword: &Keyword,
source_type: PySourceType,
) -> Option<Fix> { ) -> Option<Fix> {
// Add the assignment. // Add the assignment.
let attr = call.func.as_attribute_expr()?; let attr = call.func.as_attribute_expr()?;
@ -125,8 +129,14 @@ fn convert_inplace_argument_to_assignment(
); );
// Remove the `inplace` argument. // Remove the `inplace` argument.
let remove_argument = let remove_argument = remove_argument(
remove_argument(keyword, &call.arguments, Parentheses::Preserve, locator).ok()?; keyword,
&call.arguments,
Parentheses::Preserve,
locator,
source_type,
)
.ok()?;
Some(Fix::suggested_edits(insert_assignment, [remove_argument])) Some(Fix::suggested_edits(insert_assignment, [remove_argument]))
} }

View file

@ -523,7 +523,7 @@ pub(crate) fn not_missing(
definition: &Definition, definition: &Definition,
visibility: Visibility, visibility: Visibility,
) -> bool { ) -> bool {
if checker.is_stub() { if checker.source_type.is_stub() {
return true; return true;
} }

View file

@ -12,11 +12,14 @@ mod tests {
use anyhow::Result; use anyhow::Result;
use regex::Regex; use regex::Regex;
use ruff_python_parser::lexer::LexResult; use ruff_python_parser::lexer::LexResult;
use test_case::test_case; use test_case::test_case;
use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Diagnostic;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::AsMode;
use ruff_python_trivia::textwrap::dedent; use ruff_python_trivia::textwrap::dedent;
use ruff_source_file::Locator; use ruff_source_file::Locator;
@ -504,8 +507,9 @@ mod tests {
/// Note that all tests marked with `#[ignore]` should be considered TODOs. /// Note that all tests marked with `#[ignore]` should be considered TODOs.
fn flakes(contents: &str, expected: &[Rule]) { fn flakes(contents: &str, expected: &[Rule]) {
let contents = dedent(contents); let contents = dedent(contents);
let source_type = PySourceType::default();
let settings = Settings::for_rules(Linter::Pyflakes.rules()); let settings = Settings::for_rules(Linter::Pyflakes.rules());
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents); let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents, source_type.as_mode());
let locator = Locator::new(&contents); let locator = Locator::new(&contents);
let stylist = Stylist::from_tokens(&tokens, &locator); let stylist = Stylist::from_tokens(&tokens, &locator);
let indexer = Indexer::from_tokens(&tokens, &locator); let indexer = Indexer::from_tokens(&tokens, &locator);
@ -529,6 +533,7 @@ mod tests {
&settings, &settings,
flags::Noqa::Enabled, flags::Noqa::Enabled,
None, None,
source_type,
); );
diagnostics.sort_by_key(Diagnostic::start); diagnostics.sort_by_key(Diagnostic::start);
let actual = diagnostics let actual = diagnostics

View file

@ -1,5 +1,5 @@
use ruff_python_ast::{Expr, Ranged}; use ruff_python_ast::{Expr, PySourceType, Ranged};
use ruff_python_parser::{lexer, Mode, StringKind, Tok}; use ruff_python_parser::{lexer, AsMode, StringKind, Tok};
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
@ -52,9 +52,10 @@ impl AlwaysAutofixableViolation for FStringMissingPlaceholders {
fn find_useless_f_strings<'a>( fn find_useless_f_strings<'a>(
expr: &'a Expr, expr: &'a Expr,
locator: &'a Locator, locator: &'a Locator,
source_type: PySourceType,
) -> impl Iterator<Item = (TextRange, TextRange)> + 'a { ) -> impl Iterator<Item = (TextRange, TextRange)> + 'a {
let contents = locator.slice(expr.range()); let contents = locator.slice(expr.range());
lexer::lex_starts_at(contents, Mode::Module, expr.start()) lexer::lex_starts_at(contents, source_type.as_mode(), expr.start())
.flatten() .flatten()
.filter_map(|(tok, range)| match tok { .filter_map(|(tok, range)| match tok {
Tok::String { Tok::String {
@ -85,7 +86,9 @@ pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checke
.iter() .iter()
.any(|value| matches!(value, Expr::FormattedValue(_))) .any(|value| matches!(value, Expr::FormattedValue(_)))
{ {
for (prefix_range, tok_range) in find_useless_f_strings(expr, checker.locator()) { for (prefix_range, tok_range) in
find_useless_f_strings(expr, checker.locator(), checker.source_type)
{
let mut diagnostic = Diagnostic::new(FStringMissingPlaceholders, tok_range); let mut diagnostic = Diagnostic::new(FStringMissingPlaceholders, tok_range);
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(convert_f_string_to_regular_string( diagnostic.set_fix(convert_f_string_to_regular_string(

View file

@ -1,6 +1,6 @@
use itertools::Itertools; use itertools::Itertools;
use ruff_python_ast::{self as ast, Ranged, Stmt}; use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt};
use ruff_python_parser::{lexer, Mode, Tok}; use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
@ -62,12 +62,17 @@ impl Violation for UnusedVariable {
} }
/// Return the [`TextRange`] of the token before the next match of the predicate /// Return the [`TextRange`] of the token before the next match of the predicate
fn match_token_before<F>(location: TextSize, locator: &Locator, f: F) -> Option<TextRange> fn match_token_before<F>(
location: TextSize,
locator: &Locator,
source_type: PySourceType,
f: F,
) -> Option<TextRange>
where where
F: Fn(Tok) -> bool, F: Fn(Tok) -> bool,
{ {
let contents = locator.after(location); let contents = locator.after(location);
for ((_, range), (tok, _)) in lexer::lex_starts_at(contents, Mode::Module, location) for ((_, range), (tok, _)) in lexer::lex_starts_at(contents, source_type.as_mode(), location)
.flatten() .flatten()
.tuple_windows() .tuple_windows()
{ {
@ -80,7 +85,12 @@ where
/// Return the [`TextRange`] of the token after the next match of the predicate, skipping over /// Return the [`TextRange`] of the token after the next match of the predicate, skipping over
/// any bracketed expressions. /// any bracketed expressions.
fn match_token_after<F>(location: TextSize, locator: &Locator, f: F) -> Option<TextRange> fn match_token_after<F>(
location: TextSize,
locator: &Locator,
source_type: PySourceType,
f: F,
) -> Option<TextRange>
where where
F: Fn(Tok) -> bool, F: Fn(Tok) -> bool,
{ {
@ -91,7 +101,7 @@ where
let mut sqb_count = 0u32; let mut sqb_count = 0u32;
let mut brace_count = 0u32; let mut brace_count = 0u32;
for ((tok, _), (_, range)) in lexer::lex_starts_at(contents, Mode::Module, location) for ((tok, _), (_, range)) in lexer::lex_starts_at(contents, source_type.as_mode(), location)
.flatten() .flatten()
.tuple_windows() .tuple_windows()
{ {
@ -131,7 +141,12 @@ where
/// Return the [`TextRange`] of the token matching the predicate or the first mismatched /// Return the [`TextRange`] of the token matching the predicate or the first mismatched
/// bracket, skipping over any bracketed expressions. /// bracket, skipping over any bracketed expressions.
fn match_token_or_closing_brace<F>(location: TextSize, locator: &Locator, f: F) -> Option<TextRange> fn match_token_or_closing_brace<F>(
location: TextSize,
locator: &Locator,
source_type: PySourceType,
f: F,
) -> Option<TextRange>
where where
F: Fn(Tok) -> bool, F: Fn(Tok) -> bool,
{ {
@ -142,7 +157,7 @@ where
let mut sqb_count = 0u32; let mut sqb_count = 0u32;
let mut brace_count = 0u32; let mut brace_count = 0u32;
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, location).flatten() { for (tok, range) in lexer::lex_starts_at(contents, source_type.as_mode(), location).flatten() {
match tok { match tok {
Tok::Lpar => { Tok::Lpar => {
par_count = par_count.saturating_add(1); par_count = par_count.saturating_add(1);
@ -204,7 +219,10 @@ fn remove_unused_variable(
// If the expression is complex (`x = foo()`), remove the assignment, // If the expression is complex (`x = foo()`), remove the assignment,
// but preserve the right-hand side. // but preserve the right-hand side.
let start = target.start(); let start = target.start();
let end = match_token_after(start, checker.locator(), |tok| tok == Tok::Equal)? let end =
match_token_after(start, checker.locator(), checker.source_type, |tok| {
tok == Tok::Equal
})?
.start(); .start();
let edit = Edit::deletion(start, end); let edit = Edit::deletion(start, end);
Some(Fix::suggested(edit)) Some(Fix::suggested(edit))
@ -230,7 +248,10 @@ fn remove_unused_variable(
// but preserve the right-hand side. // but preserve the right-hand side.
let start = stmt.start(); let start = stmt.start();
let end = let end =
match_token_after(start, checker.locator(), |tok| tok == Tok::Equal)?.start(); match_token_after(start, checker.locator(), checker.source_type, |tok| {
tok == Tok::Equal
})?
.start();
let edit = Edit::deletion(start, end); let edit = Edit::deletion(start, end);
Some(Fix::suggested(edit)) Some(Fix::suggested(edit))
} else { } else {
@ -249,16 +270,21 @@ fn remove_unused_variable(
if let Some(optional_vars) = &item.optional_vars { if let Some(optional_vars) = &item.optional_vars {
if optional_vars.range() == range { if optional_vars.range() == range {
// Find the first token before the `as` keyword. // Find the first token before the `as` keyword.
let start = let start = match_token_before(
match_token_before(item.context_expr.start(), checker.locator(), |tok| { item.context_expr.start(),
tok == Tok::As checker.locator(),
})? checker.source_type,
.end(); |tok| tok == Tok::As,
)?
.end();
// Find the first colon, comma, or closing bracket after the `as` keyword. // Find the first colon, comma, or closing bracket after the `as` keyword.
let end = match_token_or_closing_brace(start, checker.locator(), |tok| { let end = match_token_or_closing_brace(
tok == Tok::Colon || tok == Tok::Comma start,
})? checker.locator(),
checker.source_type,
|tok| tok == Tok::Colon || tok == Tok::Comma,
)?
.start(); .start();
let edit = Edit::deletion(start, end); let edit = Edit::deletion(start, end);

View file

@ -2,7 +2,7 @@ use std::str::FromStr;
use ruff_python_ast::{self as ast, Constant, Expr, Ranged}; use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use ruff_python_literal::cformat::{CFormatPart, CFormatSpec, CFormatStrOrBytes, CFormatString}; use ruff_python_literal::cformat::{CFormatPart, CFormatSpec, CFormatStrOrBytes, CFormatString};
use ruff_python_parser::{lexer, Mode}; use ruff_python_parser::{lexer, AsMode};
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
@ -203,7 +203,9 @@ pub(crate) fn bad_string_format_type(checker: &mut Checker, expr: &Expr, right:
// Grab each string segment (in case there's an implicit concatenation). // Grab each string segment (in case there's an implicit concatenation).
let content = checker.locator().slice(expr.range()); let content = checker.locator().slice(expr.range());
let mut strings: Vec<TextRange> = vec![]; let mut strings: Vec<TextRange> = vec![];
for (tok, range) in lexer::lex_starts_at(content, Mode::Module, expr.start()).flatten() { for (tok, range) in
lexer::lex_starts_at(content, checker.source_type.as_mode(), expr.start()).flatten()
{
if tok.is_string() { if tok.is_string() {
strings.push(range); strings.push(range);
} else if tok.is_percent() { } else if tok.is_percent() {

View file

@ -4,7 +4,7 @@ use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use ruff_python_literal::cformat::{ use ruff_python_literal::cformat::{
CConversionFlags, CFormatPart, CFormatPrecision, CFormatQuantity, CFormatString, CConversionFlags, CFormatPart, CFormatPrecision, CFormatQuantity, CFormatString,
}; };
use ruff_python_parser::{lexer, Mode, Tok}; use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
@ -339,7 +339,7 @@ pub(crate) fn printf_string_formatting(
let mut extension = None; let mut extension = None;
for (tok, range) in lexer::lex_starts_at( for (tok, range) in lexer::lex_starts_at(
checker.locator().slice(expr.range()), checker.locator().slice(expr.range()),
Mode::Module, checker.source_type.as_mode(),
expr.start(), expr.start(),
) )
.flatten() .flatten()

View file

@ -4,8 +4,8 @@ use anyhow::{anyhow, Result};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr, Ranged}; use ruff_python_ast::{self as ast, Constant, Expr, PySourceType, Ranged};
use ruff_python_parser::{lexer, Mode}; use ruff_python_parser::{lexer, AsMode};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_source_file::Locator; use ruff_source_file::Locator;
use ruff_text_size::TextSize; use ruff_text_size::TextSize;
@ -84,6 +84,7 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, call: &ast::ExprCall)
mode.replacement_value(), mode.replacement_value(),
checker.locator(), checker.locator(),
checker.patch(Rule::RedundantOpenModes), checker.patch(Rule::RedundantOpenModes),
checker.source_type,
)); ));
} }
} }
@ -103,6 +104,7 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, call: &ast::ExprCall)
mode.replacement_value(), mode.replacement_value(),
checker.locator(), checker.locator(),
checker.patch(Rule::RedundantOpenModes), checker.patch(Rule::RedundantOpenModes),
checker.source_type,
)); ));
} }
} }
@ -169,6 +171,7 @@ fn create_check<T: Ranged>(
replacement_value: Option<&str>, replacement_value: Option<&str>,
locator: &Locator, locator: &Locator,
patch: bool, patch: bool,
source_type: PySourceType,
) -> Diagnostic { ) -> Diagnostic {
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
RedundantOpenModes { RedundantOpenModes {
@ -184,7 +187,7 @@ fn create_check<T: Ranged>(
))); )));
} else { } else {
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
create_remove_param_fix(locator, expr, mode_param).map(Fix::automatic) create_remove_param_fix(locator, expr, mode_param, source_type).map(Fix::automatic)
}); });
} }
} }
@ -195,6 +198,7 @@ fn create_remove_param_fix<T: Ranged>(
locator: &Locator, locator: &Locator,
expr: &T, expr: &T,
mode_param: &Expr, mode_param: &Expr,
source_type: PySourceType,
) -> Result<Edit> { ) -> Result<Edit> {
let content = locator.slice(expr.range()); let content = locator.slice(expr.range());
// Find the last comma before mode_param and create a deletion fix // Find the last comma before mode_param and create a deletion fix
@ -203,7 +207,8 @@ fn create_remove_param_fix<T: Ranged>(
let mut fix_end: Option<TextSize> = None; let mut fix_end: Option<TextSize> = None;
let mut is_first_arg: bool = false; let mut is_first_arg: bool = false;
let mut delete_first_arg: bool = false; let mut delete_first_arg: bool = false;
for (tok, range) in lexer::lex_starts_at(content, Mode::Module, expr.start()).flatten() { for (tok, range) in lexer::lex_starts_at(content, source_type.as_mode(), expr.start()).flatten()
{
if range.start() == mode_param.start() { if range.start() == mode_param.start() {
if is_first_arg { if is_first_arg {
delete_first_arg = true; delete_first_arg = true;

View file

@ -2,7 +2,7 @@ use anyhow::Result;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Keyword, Ranged}; use ruff_python_ast::{self as ast, Keyword, PySourceType, Ranged};
use ruff_source_file::Locator; use ruff_source_file::Locator;
use crate::autofix::edits::{remove_argument, Parentheses}; use crate::autofix::edits::{remove_argument, Parentheses};
@ -56,6 +56,7 @@ fn generate_fix(
stderr: &Keyword, stderr: &Keyword,
call: &ast::ExprCall, call: &ast::ExprCall,
locator: &Locator, locator: &Locator,
source_type: PySourceType,
) -> Result<Fix> { ) -> Result<Fix> {
let (first, second) = if stdout.start() < stderr.start() { let (first, second) = if stdout.start() < stderr.start() {
(stdout, stderr) (stdout, stderr)
@ -69,6 +70,7 @@ fn generate_fix(
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
locator, locator,
source_type,
)?], )?],
)) ))
} }
@ -103,7 +105,9 @@ pub(crate) fn replace_stdout_stderr(checker: &mut Checker, call: &ast::ExprCall)
let mut diagnostic = Diagnostic::new(ReplaceStdoutStderr, call.range()); let mut diagnostic = Diagnostic::new(ReplaceStdoutStderr, call.range());
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| generate_fix(stdout, stderr, call, checker.locator())); diagnostic.try_set_fix(|| {
generate_fix(stdout, stderr, call, checker.locator(), checker.source_type)
});
} }
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }

View file

@ -1,7 +1,7 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Keyword, Ranged}; use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Keyword, PySourceType, Ranged};
use ruff_python_parser::{lexer, Mode, Tok}; use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_source_file::Locator; use ruff_source_file::Locator;
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
@ -119,12 +119,18 @@ fn match_encoding_arg(arguments: &Arguments) -> Option<EncodingArg> {
} }
/// Return a [`Fix`] replacing the call to encode with a byte string. /// Return a [`Fix`] replacing the call to encode with a byte string.
fn replace_with_bytes_literal<T: Ranged>(locator: &Locator, expr: &T) -> Fix { fn replace_with_bytes_literal<T: Ranged>(
locator: &Locator,
expr: &T,
source_type: PySourceType,
) -> Fix {
// Build up a replacement string by prefixing all string tokens with `b`. // Build up a replacement string by prefixing all string tokens with `b`.
let contents = locator.slice(expr.range()); let contents = locator.slice(expr.range());
let mut replacement = String::with_capacity(contents.len() + 1); let mut replacement = String::with_capacity(contents.len() + 1);
let mut prev = expr.start(); let mut prev = expr.start();
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, expr.start()).flatten() { for (tok, range) in
lexer::lex_starts_at(contents, source_type.as_mode(), expr.start()).flatten()
{
match tok { match tok {
Tok::Dot => break, Tok::Dot => break,
Tok::String { .. } => { Tok::String { .. } => {
@ -166,7 +172,11 @@ pub(crate) fn unnecessary_encode_utf8(checker: &mut Checker, call: &ast::ExprCal
call.range(), call.range(),
); );
if checker.patch(Rule::UnnecessaryEncodeUTF8) { if checker.patch(Rule::UnnecessaryEncodeUTF8) {
diagnostic.set_fix(replace_with_bytes_literal(checker.locator(), call)); diagnostic.set_fix(replace_with_bytes_literal(
checker.locator(),
call,
checker.source_type,
));
} }
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} else if let EncodingArg::Keyword(kwarg) = encoding_arg { } else if let EncodingArg::Keyword(kwarg) = encoding_arg {
@ -185,6 +195,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &mut Checker, call: &ast::ExprCal
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator(), checker.locator(),
checker.source_type,
) )
.map(Fix::automatic) .map(Fix::automatic)
}); });
@ -205,6 +216,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &mut Checker, call: &ast::ExprCal
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator(), checker.locator(),
checker.source_type,
) )
.map(Fix::automatic) .map(Fix::automatic)
}); });
@ -232,6 +244,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &mut Checker, call: &ast::ExprCal
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator(), checker.locator(),
checker.source_type,
) )
.map(Fix::automatic) .map(Fix::automatic)
}); });
@ -252,6 +265,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &mut Checker, call: &ast::ExprCal
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator(), checker.locator(),
checker.source_type,
) )
.map(Fix::automatic) .map(Fix::automatic)
}); });

View file

@ -69,8 +69,14 @@ pub(crate) fn useless_object_inheritance(checker: &mut Checker, class_def: &ast:
); );
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
remove_argument(base, arguments, Parentheses::Remove, checker.locator()) remove_argument(
.map(Fix::automatic) base,
arguments,
Parentheses::Remove,
checker.locator(),
checker.source_type,
)
.map(Fix::automatic)
}); });
} }
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);

View file

@ -7,11 +7,14 @@ use std::path::Path;
use anyhow::Result; use anyhow::Result;
use itertools::Itertools; use itertools::Itertools;
use ruff_python_parser::lexer::LexResult; use ruff_python_parser::lexer::LexResult;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic}; use ruff_diagnostics::{AutofixKind, Diagnostic};
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::AsMode;
use ruff_python_trivia::textwrap::dedent; use ruff_python_trivia::textwrap::dedent;
use ruff_source_file::{Locator, SourceFileBuilder}; use ruff_source_file::{Locator, SourceFileBuilder};
@ -61,8 +64,9 @@ pub(crate) fn test_notebook_path(
path: impl AsRef<Path>, path: impl AsRef<Path>,
expected: impl AsRef<Path>, expected: impl AsRef<Path>,
settings: &Settings, settings: &Settings,
) -> Result<(Vec<Message>, SourceKind)> { ) -> Result<(Vec<Message>, SourceKind, SourceKind)> {
let mut source_kind = SourceKind::Jupyter(read_jupyter_notebook(path.as_ref())?); let mut source_kind = SourceKind::Jupyter(read_jupyter_notebook(path.as_ref())?);
let original_source_kind = source_kind.clone();
let messages = test_contents(&mut source_kind, path.as_ref(), settings); let messages = test_contents(&mut source_kind, path.as_ref(), settings);
let expected_notebook = read_jupyter_notebook(expected.as_ref())?; let expected_notebook = read_jupyter_notebook(expected.as_ref())?;
if let SourceKind::Jupyter(notebook) = &source_kind { if let SourceKind::Jupyter(notebook) = &source_kind {
@ -70,7 +74,7 @@ pub(crate) fn test_notebook_path(
assert_eq!(notebook.index(), expected_notebook.index()); assert_eq!(notebook.index(), expected_notebook.index());
assert_eq!(notebook.content(), expected_notebook.content()); assert_eq!(notebook.content(), expected_notebook.content());
}; };
Ok((messages, source_kind)) Ok((messages, original_source_kind, source_kind))
} }
/// Run [`check_path`] on a snippet of Python code. /// Run [`check_path`] on a snippet of Python code.
@ -100,7 +104,8 @@ pub(crate) fn max_iterations() -> usize {
/// asserts that autofixes converge after a fixed number of iterations. /// asserts that autofixes converge after a fixed number of iterations.
fn test_contents(source_kind: &mut SourceKind, path: &Path, settings: &Settings) -> Vec<Message> { fn test_contents(source_kind: &mut SourceKind, path: &Path, settings: &Settings) -> Vec<Message> {
let contents = source_kind.content().to_string(); let contents = source_kind.content().to_string();
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents); let source_type = PySourceType::from(path);
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents, source_type.as_mode());
let locator = Locator::new(&contents); let locator = Locator::new(&contents);
let stylist = Stylist::from_tokens(&tokens, &locator); let stylist = Stylist::from_tokens(&tokens, &locator);
let indexer = Indexer::from_tokens(&tokens, &locator); let indexer = Indexer::from_tokens(&tokens, &locator);
@ -125,6 +130,7 @@ fn test_contents(source_kind: &mut SourceKind, path: &Path, settings: &Settings)
settings, settings,
flags::Noqa::Enabled, flags::Noqa::Enabled,
Some(source_kind), Some(source_kind),
source_type,
); );
let source_has_errors = error.is_some(); let source_has_errors = error.is_some();
@ -162,7 +168,8 @@ fn test_contents(source_kind: &mut SourceKind, path: &Path, settings: &Settings)
notebook.update(&source_map, &fixed_contents); notebook.update(&source_map, &fixed_contents);
}; };
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&fixed_contents); let tokens: Vec<LexResult> =
ruff_python_parser::tokenize(&fixed_contents, source_type.as_mode());
let locator = Locator::new(&fixed_contents); let locator = Locator::new(&fixed_contents);
let stylist = Stylist::from_tokens(&tokens, &locator); let stylist = Stylist::from_tokens(&tokens, &locator);
let indexer = Indexer::from_tokens(&tokens, &locator); let indexer = Indexer::from_tokens(&tokens, &locator);
@ -187,6 +194,7 @@ fn test_contents(source_kind: &mut SourceKind, path: &Path, settings: &Settings)
settings, settings,
flags::Noqa::Enabled, flags::Noqa::Enabled,
Some(source_kind), Some(source_kind),
source_type,
); );
if let Some(fixed_error) = fixed_error { if let Some(fixed_error) = fixed_error {

View file

@ -9,6 +9,7 @@ use ruff::linter::lint_only;
use ruff::settings::{flags, Settings}; use ruff::settings::{flags, Settings};
use ruff::RuleSelector; use ruff::RuleSelector;
use ruff_benchmark::{TestCase, TestCaseSpeed, TestFile, TestFileDownloadError}; use ruff_benchmark::{TestCase, TestCaseSpeed, TestFile, TestFileDownloadError};
use ruff_python_ast::PySourceType;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[global_allocator] #[global_allocator]
@ -57,13 +58,15 @@ fn benchmark_linter(mut group: BenchmarkGroup<WallTime>, settings: &Settings) {
&case, &case,
|b, case| { |b, case| {
b.iter(|| { b.iter(|| {
let path = case.path();
let result = lint_only( let result = lint_only(
case.code(), case.code(),
&case.path(), &path,
None, None,
settings, settings,
flags::Noqa::Enabled, flags::Noqa::Enabled,
None, None,
PySourceType::from(path.as_path()),
); );
// Assert that file contains no parse errors // Assert that file contains no parse errors

View file

@ -29,7 +29,8 @@ use ruff::{fs, IOError};
use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Diagnostic;
use ruff_macros::CacheKey; use ruff_macros::CacheKey;
use ruff_python_ast::imports::ImportMap; use ruff_python_ast::imports::ImportMap;
use ruff_python_stdlib::path::{is_jupyter_notebook, is_project_toml}; use ruff_python_ast::PySourceType;
use ruff_python_stdlib::path::is_project_toml;
use ruff_source_file::{LineIndex, SourceCode, SourceFileBuilder}; use ruff_source_file::{LineIndex, SourceCode, SourceFileBuilder};
#[derive(CacheKey)] #[derive(CacheKey)]
@ -211,8 +212,10 @@ pub(crate) fn lint_path(
}); });
} }
let source_type = PySourceType::from(path);
// Read the file from disk // Read the file from disk
let mut source_kind = if is_jupyter_notebook(path) { let mut source_kind = if source_type.is_jupyter() {
match load_jupyter_notebook(path) { match load_jupyter_notebook(path) {
Ok(notebook) => SourceKind::Jupyter(notebook), Ok(notebook) => SourceKind::Jupyter(notebook),
Err(diagnostic) => return Ok(*diagnostic), Err(diagnostic) => return Ok(*diagnostic),
@ -249,6 +252,7 @@ pub(crate) fn lint_path(
noqa, noqa,
&settings.lib, &settings.lib,
&mut source_kind, &mut source_kind,
source_type,
) { ) {
if !fixed.is_empty() { if !fixed.is_empty() {
match autofix { match autofix {
@ -335,6 +339,7 @@ pub(crate) fn lint_path(
&settings.lib, &settings.lib,
noqa, noqa,
Some(&source_kind), Some(&source_kind),
source_type,
); );
let fixed = FxHashMap::default(); let fixed = FxHashMap::default();
(result, fixed) (result, fixed)
@ -347,6 +352,7 @@ pub(crate) fn lint_path(
&settings.lib, &settings.lib,
noqa, noqa,
Some(&source_kind), Some(&source_kind),
source_type,
); );
let fixed = FxHashMap::default(); let fixed = FxHashMap::default();
(result, fixed) (result, fixed)
@ -396,6 +402,8 @@ pub(crate) fn lint_stdin(
autofix: flags::FixMode, autofix: flags::FixMode,
) -> Result<Diagnostics> { ) -> Result<Diagnostics> {
let mut source_kind = SourceKind::Python(contents.to_string()); let mut source_kind = SourceKind::Python(contents.to_string());
let source_type = PySourceType::default();
// Lint the inputs. // Lint the inputs.
let ( let (
LinterResult { LinterResult {
@ -415,6 +423,7 @@ pub(crate) fn lint_stdin(
noqa, noqa,
settings, settings,
&mut source_kind, &mut source_kind,
source_type,
) { ) {
match autofix { match autofix {
flags::FixMode::Apply => { flags::FixMode::Apply => {
@ -450,6 +459,7 @@ pub(crate) fn lint_stdin(
settings, settings,
noqa, noqa,
Some(&source_kind), Some(&source_kind),
source_type,
); );
let fixed = FxHashMap::default(); let fixed = FxHashMap::default();
@ -468,6 +478,7 @@ pub(crate) fn lint_stdin(
settings, settings,
noqa, noqa,
Some(&source_kind), Some(&source_kind),
source_type,
); );
let fixed = FxHashMap::default(); let fixed = FxHashMap::default();
(result, fixed) (result, fixed)

View file

@ -5,18 +5,26 @@ use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::Result; use anyhow::Result;
use ruff_python_parser::parse_suite; use ruff_python_parser::{parse, Mode};
#[derive(clap::Args)] #[derive(clap::Args)]
pub(crate) struct Args { pub(crate) struct Args {
/// Python file for which to generate the AST. /// Python file for which to generate the AST.
#[arg(required = true)] #[arg(required = true)]
file: PathBuf, file: PathBuf,
/// Run in Jupyter mode i.e., allow line magics.
#[arg(long)]
jupyter: bool,
} }
pub(crate) fn main(args: &Args) -> Result<()> { pub(crate) fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?; let contents = fs::read_to_string(&args.file)?;
let python_ast = parse_suite(&contents, &args.file.to_string_lossy())?; let mode = if args.jupyter {
Mode::Jupyter
} else {
Mode::Module
};
let python_ast = parse(&contents, mode, &args.file.to_string_lossy())?;
println!("{python_ast:#?}"); println!("{python_ast:#?}");
Ok(()) Ok(())
} }

View file

@ -12,11 +12,19 @@ pub(crate) struct Args {
/// Python file for which to generate the AST. /// Python file for which to generate the AST.
#[arg(required = true)] #[arg(required = true)]
file: PathBuf, file: PathBuf,
/// Run in Jupyter mode i.e., allow line magics (`%`, `!`, `?`, `/`, `,`, `;`).
#[arg(long)]
jupyter: bool,
} }
pub(crate) fn main(args: &Args) -> Result<()> { pub(crate) fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?; let contents = fs::read_to_string(&args.file)?;
for (tok, range) in lexer::lex(&contents, Mode::Module).flatten() { let mode = if args.jupyter {
Mode::Jupyter
} else {
Mode::Module
};
for (tok, range) in lexer::lex(&contents, mode).flatten() {
println!( println!(
"{start:#?} {tok:#?} {end:#?}", "{start:#?} {tok:#?} {end:#?}",
start = range.start(), start = range.start(),

View file

@ -114,7 +114,7 @@ pub use parser::{
parse, parse_expression, parse_expression_starts_at, parse_program, parse_starts_at, parse, parse_expression, parse_expression_starts_at, parse_program, parse_starts_at,
parse_suite, parse_tokens, ParseError, ParseErrorType, parse_suite, parse_tokens, ParseError, ParseErrorType,
}; };
use ruff_python_ast::{CmpOp, Expr, Mod, Ranged, Suite}; use ruff_python_ast::{CmpOp, Expr, Mod, PySourceType, Ranged, Suite};
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
pub use string::FStringErrorType; pub use string::FStringErrorType;
pub use token::{StringKind, Tok, TokenKind}; pub use token::{StringKind, Tok, TokenKind};
@ -130,9 +130,9 @@ mod token;
pub mod typing; pub mod typing;
/// Collect tokens up to and including the first error. /// Collect tokens up to and including the first error.
pub fn tokenize(contents: &str) -> Vec<LexResult> { pub fn tokenize(contents: &str, mode: Mode) -> Vec<LexResult> {
let mut tokens: Vec<LexResult> = vec![]; let mut tokens: Vec<LexResult> = vec![];
for tok in lexer::lex(contents, Mode::Module) { for tok in lexer::lex(contents, mode) {
let is_err = tok.is_err(); let is_err = tok.is_err();
tokens.push(tok); tokens.push(tok);
if is_err { if is_err {
@ -146,17 +146,32 @@ pub fn tokenize(contents: &str) -> Vec<LexResult> {
pub fn parse_program_tokens( pub fn parse_program_tokens(
lxr: Vec<LexResult>, lxr: Vec<LexResult>,
source_path: &str, source_path: &str,
is_jupyter_notebook: bool,
) -> anyhow::Result<Suite, ParseError> { ) -> anyhow::Result<Suite, ParseError> {
match parse_tokens(lxr, Mode::Module, source_path)? { let mode = if is_jupyter_notebook {
Mode::Jupyter
} else {
Mode::Module
};
match parse_tokens(lxr, mode, source_path)? {
Mod::Module(m) => Ok(m.body), Mod::Module(m) => Ok(m.body),
Mod::Expression(_) => unreachable!("Mode::Module doesn't return other variant"), Mod::Expression(_) => unreachable!("Mode::Module doesn't return other variant"),
} }
} }
/// Return the `Range` of the first `Tok::Colon` token in a `Range`. /// Return the `Range` of the first `Tok::Colon` token in a `Range`.
pub fn first_colon_range(range: TextRange, source: &str) -> Option<TextRange> { pub fn first_colon_range(
range: TextRange,
source: &str,
is_jupyter_notebook: bool,
) -> Option<TextRange> {
let contents = &source[range]; let contents = &source[range];
let range = lexer::lex_starts_at(contents, Mode::Module, range.start()) let mode = if is_jupyter_notebook {
Mode::Jupyter
} else {
Mode::Module
};
let range = lexer::lex_starts_at(contents, mode, range.start())
.flatten() .flatten()
.find(|(tok, _)| tok.is_colon()) .find(|(tok, _)| tok.is_colon())
.map(|(_, range)| range); .map(|(_, range)| range);
@ -308,6 +323,19 @@ impl std::str::FromStr for Mode {
} }
} }
pub trait AsMode {
fn as_mode(&self) -> Mode;
}
impl AsMode for PySourceType {
fn as_mode(&self) -> Mode {
match self {
PySourceType::Python | PySourceType::Stub => Mode::Module,
PySourceType::Jupyter => Mode::Jupyter,
}
}
}
/// Returned when a given mode is not valid. /// Returned when a given mode is not valid.
#[derive(Debug)] #[derive(Debug)]
pub struct ModeParseError; pub struct ModeParseError;
@ -357,6 +385,7 @@ mod tests {
let range = first_colon_range( let range = first_colon_range(
TextRange::new(TextSize::from(0), contents.text_len()), TextRange::new(TextSize::from(0), contents.text_len()),
contents, contents,
false,
) )
.unwrap(); .unwrap();
assert_eq!(&contents[range], ":"); assert_eq!(&contents[range], ":");

View file

@ -36,6 +36,7 @@ use regex::Regex;
use ruff_python_ast::statement_visitor::{walk_body, walk_stmt, StatementVisitor}; use ruff_python_ast::statement_visitor::{walk_body, walk_stmt, StatementVisitor};
use ruff_python_ast::visitor::{walk_expr, Visitor}; use ruff_python_ast::visitor::{walk_expr, Visitor};
use ruff_python_ast::{Expr, Ranged, Stmt, Suite}; use ruff_python_ast::{Expr, Ranged, Stmt, Suite};
use ruff_python_parser::Mode;
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -275,7 +276,7 @@ impl Strategy for StrategyRemoveToken {
input: &'a str, input: &'a str,
_ast: &'a Suite, _ast: &'a Suite,
) -> Result<Box<dyn ExactSizeStringIter + 'a>> { ) -> Result<Box<dyn ExactSizeStringIter + 'a>> {
let token_ranges: Vec<_> = ruff_python_parser::tokenize(input) let token_ranges: Vec<_> = ruff_python_parser::tokenize(input, Mode::Module)
.into_iter() .into_iter()
// At this point we know we have valid python code // At this point we know we have valid python code
.map(Result::unwrap) .map(Result::unwrap)
@ -320,9 +321,9 @@ fn minimization_step(
pattern: &Regex, pattern: &Regex,
last_strategy_and_idx: Option<(&'static dyn Strategy, usize)>, last_strategy_and_idx: Option<(&'static dyn Strategy, usize)>,
) -> Result<Option<(&'static dyn Strategy, usize, String)>> { ) -> Result<Option<(&'static dyn Strategy, usize, String)>> {
let tokens = ruff_python_parser::tokenize(input); let tokens = ruff_python_parser::tokenize(input, Mode::Module);
let ast = let ast = ruff_python_parser::parse_program_tokens(tokens, "input.py", false)
ruff_python_parser::parse_program_tokens(tokens, "input.py").context("not valid python")?; .context("not valid python")?;
// Try the last succeeding strategy first, skipping all that failed last time // Try the last succeeding strategy first, skipping all that failed last time
if let Some((last_strategy, last_idx)) = last_strategy_and_idx { if let Some((last_strategy, last_idx)) = last_strategy_and_idx {

View file

@ -25,6 +25,7 @@ use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_formatter::{format_module, format_node, PyFormatOptions}; use ruff_python_formatter::{format_module, format_node, PyFormatOptions};
use ruff_python_index::{CommentRangesBuilder, Indexer}; use ruff_python_index::{CommentRangesBuilder, Indexer};
use ruff_python_parser::AsMode;
use ruff_source_file::{Locator, SourceLocation}; use ruff_source_file::{Locator, SourceLocation};
#[wasm_bindgen(typescript_custom_section)] #[wasm_bindgen(typescript_custom_section)]
@ -197,8 +198,10 @@ impl Workspace {
} }
pub fn check(&self, contents: &str) -> Result<JsValue, Error> { pub fn check(&self, contents: &str) -> Result<JsValue, Error> {
let source_type = PySourceType::default();
// Tokenize once. // Tokenize once.
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents); let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, source_type.as_mode());
// Map row and column locations to byte slices (lazily). // Map row and column locations to byte slices (lazily).
let locator = Locator::new(contents); let locator = Locator::new(contents);
@ -228,6 +231,7 @@ impl Workspace {
&self.settings, &self.settings,
flags::Noqa::Enabled, flags::Noqa::Enabled,
None, None,
source_type,
); );
let source_code = locator.to_source_code(); let source_code = locator.to_source_code();