Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Detect several simple syntax errors in the parser #16308

Closed
wants to merge 37 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6ba7166
add unused Parser::syntax_errors field
ntBre Feb 10, 2025
cab545f
add hard-coded python version and detect `match`
ntBre Feb 10, 2025
8f0bd4c
make PythonVersion more public, convert SyntaxError in red-knot
ntBre Feb 10, 2025
846314f
process SyntaxErrors in ruff
ntBre Feb 10, 2025
4712ff4
pass target version at the very end
ntBre Feb 10, 2025
27e9461
pass tests
ntBre Feb 10, 2025
625682e
add ruff test case
ntBre Feb 10, 2025
e2614be
detect late future imports in the parser
ntBre Feb 10, 2025
4423bd4
pass f404 tests
ntBre Feb 10, 2025
6ee2eb8
check if rules are enabled before converting to diagnostics
ntBre Feb 10, 2025
a1b0aa6
add a todo about duplicate diagnostics
ntBre Feb 10, 2025
5c24655
clippy
ntBre Feb 10, 2025
44a6c66
update to use ast::PythonVersion and Span
ntBre Feb 14, 2025
4bb914c
remove LateFutureImport detection from the parser
ntBre Feb 15, 2025
1a1b2ad
tidy up
ntBre Feb 15, 2025
723cf7e
clean up after rebase
ntBre Feb 19, 2025
832e0a7
add ParseOptions::target_version
ntBre Feb 19, 2025
d1daafa
filter syntax errors when parsing, store version on SyntaxError
ntBre Feb 19, 2025
a0efadd
make ParseOptions Clone
ntBre Feb 19, 2025
4dba857
pass ParseOptions::target_version in the linter
ntBre Feb 19, 2025
a3f8ea9
pass ParseOptions::target_version in red-knot
ntBre Feb 19, 2025
3f43c82
ignore version-related syntax errors when fuzzing
ntBre Feb 19, 2025
3a5de09
Red-knot no longer panics!
AlexWaygood Feb 21, 2025
e47316e
Fix fuzzing script and fix Rust formatting
AlexWaygood Feb 21, 2025
a50fa2a
Revert "ignore version-related syntax errors when fuzzing"
ntBre Feb 21, 2025
a156781
pass check_file_skips_type_checking_when by initializing settings
ntBre Feb 21, 2025
114c207
fix clippy lint about &impl ToString
ntBre Feb 21, 2025
5b55d04
Merge branch 'main' of github.com:astral-sh/ruff into brent/syntax-er…
ntBre Feb 21, 2025
b06e1b1
pass version to new parsed_module call
ntBre Feb 21, 2025
2eecea8
WIP
AlexWaygood Feb 21, 2025
53ed4ac
add ExceptStarBeforePy311 variant, refactor SyntaxError::message
ntBre Feb 21, 2025
c832d1d
use latest version `ruff_linter_fixtures` to avoid syntax errors
ntBre Feb 21, 2025
f5a607c
test except*
ntBre Feb 21, 2025
d6c2508
factor out syntax_errors!, add `add_syntax_error`, and typebefore312
ntBre Feb 21, 2025
8763bee
test type statement
ntBre Feb 21, 2025
832274b
detect type parameter lists
ntBre Feb 21, 2025
35c7cd0
detect type parameter defaults
ntBre Feb 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 23 additions & 2 deletions crates/red_knot_project/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use files::{Index, Indexed, IndexedFiles};
use metadata::settings::Settings;
pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection};
use red_knot_python_semantic::register_lints;
use red_knot_python_semantic::syntax::SyntaxDiagnostic;
use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::{register_lints, Program};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity, Span};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::parsed::parsed_module;
Expand Down Expand Up @@ -333,12 +334,20 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
return diagnostics;
}

let parsed = parsed_module(db.upcast(), file);
let parsed = parsed_module(db.upcast(), file, Program::get(db).python_version(db));
diagnostics.extend(parsed.errors().iter().map(|error| {
let diagnostic: Box<dyn Diagnostic> = Box::new(ParseDiagnostic::new(file, error.clone()));
diagnostic
}));

if parsed.is_valid() {
diagnostics.extend(parsed.syntax_errors().iter().map(|error| {
let diagnostic: Box<dyn Diagnostic> =
Box::new(SyntaxDiagnostic::from_syntax_error(error, file));
diagnostic
}));
}

diagnostics.extend(check_types(db.upcast(), file).iter().map(|diagnostic| {
let boxed: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
boxed
Expand Down Expand Up @@ -477,19 +486,31 @@ mod tests {
use crate::db::tests::TestDb;
use crate::{check_file_impl, ProjectMetadata};
use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::system_path_to_file;
use ruff_db::source::source_text;
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf};
use ruff_db::testing::assert_function_query_was_not_run;
use ruff_python_ast::name::Name;
use ruff_python_ast::PythonVersion;

#[test]
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
let project = ProjectMetadata::new(Name::new_static("test"), SystemPathBuf::from("/"));
let mut db = TestDb::new(project);
let path = SystemPath::new("test.py");

Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(vec![SystemPathBuf::from(".")]),
},
)
.expect("Failed to configure program settings");

db.write_file(path, "x = 10")?;
let file = system_path_to_file(&db, path).unwrap();

Expand Down
4 changes: 2 additions & 2 deletions crates/red_knot_project/tests/check.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anyhow::{anyhow, Context};
use red_knot_project::{ProjectDatabase, ProjectMetadata};
use red_knot_python_semantic::{HasType, SemanticModel};
use red_knot_python_semantic::{HasType, Program, SemanticModel};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::parsed::parsed_module;
use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem};
Expand Down Expand Up @@ -165,7 +165,7 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> {
fn pull_types(db: &ProjectDatabase, file: File) {
let mut visitor = PullTypesVisitor::new(db, file);

let ast = parsed_module(db, file);
let ast = parsed_module(db, file, Program::get(db).python_version(db));

visitor.visit_body(ast.suite());
}
Expand Down
1 change: 1 addition & 0 deletions crates/red_knot_python_semantic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ mod semantic_model;
pub(crate) mod site_packages;
mod suppression;
pub(crate) mod symbol;
pub mod syntax;
pub mod types;
mod unpack;
mod util;
Expand Down
27 changes: 15 additions & 12 deletions crates/red_knot_python_semantic/src/semantic_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::semantic_index::symbol::{
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId, SymbolTable,
};
use crate::semantic_index::use_def::{EagerBindingsKey, ScopedEagerBindingsId, UseDefMap};
use crate::Db;
use crate::{Db, Program};

pub mod ast_ids;
pub mod attribute_assignment;
Expand All @@ -45,7 +45,7 @@ type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), FxBuildHasher>;
pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> {
let _span = tracing::trace_span!("semantic_index", file = %file.path(db)).entered();

let parsed = parsed_module(db.upcast(), file);
let parsed = parsed_module(db.upcast(), file, Program::get(db).python_version(db));

SemanticIndexBuilder::new(db, file, parsed).build()
}
Expand Down Expand Up @@ -407,11 +407,10 @@ impl FusedIterator for ChildrenIter<'_> {}
mod tests {
use ruff_db::files::{system_path_to_file, File};
use ruff_db::parsed::parsed_module;
use ruff_db::system::DbWithTestSystem;
use ruff_python_ast as ast;
use ruff_python_ast::{self as ast, PythonVersion};
use ruff_text_size::{Ranged, TextRange};

use crate::db::tests::TestDb;
use crate::db::tests::{TestDb, TestDbBuilder};
use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId};
use crate::semantic_index::definition::{Definition, DefinitionKind};
use crate::semantic_index::symbol::{
Expand All @@ -438,11 +437,15 @@ mod tests {
file: File,
}

fn test_case(content: impl ToString) -> TestCase {
let mut db = TestDb::new();
db.write_file("test.py", content).unwrap();
fn test_case(content: &str) -> TestCase {
const FILENAME: &str = "test.py";

let file = system_path_to_file(&db, "test.py").unwrap();
let db = TestDbBuilder::new()
.with_file(FILENAME, content)
.build()
.unwrap();

let file = system_path_to_file(&db, FILENAME).unwrap();

TestCase { db, file }
}
Expand Down Expand Up @@ -829,7 +832,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):

let use_def = index.use_def_map(comprehension_scope_id);

let module = parsed_module(&db, file).syntax();
let module = parsed_module(&db, file, PythonVersion::default()).syntax();
let element = module.body[0]
.as_expr_stmt()
.unwrap()
Expand Down Expand Up @@ -1078,7 +1081,7 @@ class C[T]:
#[test]
fn reachability_trivial() {
let TestCase { db, file } = test_case("x = 1; x");
let parsed = parsed_module(&db, file);
let parsed = parsed_module(&db, file, PythonVersion::default());
let scope = global_scope(&db, file);
let ast = parsed.syntax();
let ast::Stmt::Expr(ast::StmtExpr {
Expand Down Expand Up @@ -1111,7 +1114,7 @@ class C[T]:
let TestCase { db, file } = test_case("x = 1;\ndef test():\n y = 4");

let index = semantic_index(&db, file);
let parsed = parsed_module(&db, file);
let parsed = parsed_module(&db, file, PythonVersion::default());
let ast = parsed.syntax();

let x_stmt = ast.body[0].as_assign_stmt().unwrap();
Expand Down
7 changes: 4 additions & 3 deletions crates/red_knot_python_semantic/src/semantic_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ impl_binding_has_ty!(ast::ParameterWithDefault);
mod tests {
use ruff_db::files::system_path_to_file;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::PythonVersion;

use crate::db::tests::TestDbBuilder;
use crate::{HasType, SemanticModel};
Expand All @@ -178,7 +179,7 @@ mod tests {

let foo = system_path_to_file(&db, "/src/foo.py").unwrap();

let ast = parsed_module(&db, foo);
let ast = parsed_module(&db, foo, PythonVersion::default());

let function = ast.suite()[0].as_function_def_stmt().unwrap();
let model = SemanticModel::new(&db, foo);
Expand All @@ -197,7 +198,7 @@ mod tests {

let foo = system_path_to_file(&db, "/src/foo.py").unwrap();

let ast = parsed_module(&db, foo);
let ast = parsed_module(&db, foo, PythonVersion::default());

let class = ast.suite()[0].as_class_def_stmt().unwrap();
let model = SemanticModel::new(&db, foo);
Expand All @@ -217,7 +218,7 @@ mod tests {

let bar = system_path_to_file(&db, "/src/bar.py").unwrap();

let ast = parsed_module(&db, bar);
let ast = parsed_module(&db, bar, PythonVersion::default());

let import = ast.suite()[0].as_import_from_stmt().unwrap();
let alias = &import.names[0];
Expand Down
3 changes: 2 additions & 1 deletion crates/red_knot_python_semantic/src/suppression.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::lint::{GetLintError, Level, LintMetadata, LintRegistry, LintStatus};
use crate::types::{TypeCheckDiagnostic, TypeCheckDiagnostics};
use crate::Program;
use crate::{declare_lint, lint::LintId, Db};
use ruff_db::diagnostic::DiagnosticId;
use ruff_db::{files::File, parsed::parsed_module, source::source_text};
Expand Down Expand Up @@ -88,7 +89,7 @@ declare_lint! {

#[salsa::tracked(return_ref)]
pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions {
let parsed = parsed_module(db.upcast(), file);
let parsed = parsed_module(db.upcast(), file, Program::get(db).python_version(db));
let source = source_text(db.upcast(), file);

let mut builder = SuppressionsBuilder::new(&source, db.lint_registry());
Expand Down
43 changes: 43 additions & 0 deletions crates/red_knot_python_semantic/src/syntax.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::borrow::Cow;

use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity, Span};
use ruff_db::files::File;
use ruff_python_parser::SyntaxError;
use ruff_text_size::TextRange;

#[derive(Debug, Eq, PartialEq, Clone)]
pub struct SyntaxDiagnostic {
id: DiagnosticId,
message: String,
file: File,
range: TextRange,
}

impl Diagnostic for SyntaxDiagnostic {
fn id(&self) -> ruff_db::diagnostic::DiagnosticId {
self.id
}

fn message(&self) -> Cow<str> {
Cow::from(&self.message)
}

fn severity(&self) -> ruff_db::diagnostic::Severity {
Severity::Error
}

fn span(&self) -> Option<ruff_db::diagnostic::Span> {
Some(Span::from(self.file).with_range(self.range))
}
}

impl SyntaxDiagnostic {
pub fn from_syntax_error(value: &SyntaxError, file: File) -> Self {
Self {
id: DiagnosticId::invalid_syntax(Some(value.kind.as_str())),
message: value.message(),
file,
range: value.range,
}
}
}
4 changes: 3 additions & 1 deletion crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5445,7 +5445,9 @@ pub(crate) mod tests {
);
let events = db.take_salsa_events();

let call = &*parsed_module(&db, bar).syntax().body[1]
let call = &*parsed_module(&db, bar, Program::get(&db).python_version(&db))
.syntax()
.body[1]
.as_assign_stmt()
.unwrap()
.value;
Expand Down
18 changes: 12 additions & 6 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ use crate::types::{
};
use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice};
use crate::Db;
use crate::{Db, Program};

use super::call::CallError;
use super::context::{InNoTypeCheck, InferContext, WithDiagnostics};
Expand Down Expand Up @@ -538,10 +538,15 @@ impl<'db> TypeInferenceBuilder<'db> {
}

fn infer_region_scope(&mut self, scope: ScopeId<'db>) {
let node = scope.node(self.db());
let db = self.db();
let node = scope.node(db);
match node {
NodeWithScopeKind::Module => {
let parsed = parsed_module(self.db().upcast(), self.file());
let parsed = parsed_module(
db.upcast(),
self.file(),
Program::get(db).python_version(db),
);
self.infer_module(parsed.syntax());
}
NodeWithScopeKind::Function(function) => self.infer_function_body(function.node()),
Expand Down Expand Up @@ -576,7 +581,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// Infer the deferred types for the definitions here to consider the end-of-scope
// semantics.
for definition in std::mem::take(&mut self.types.deferred) {
self.extend(infer_deferred_types(self.db(), definition));
self.extend(infer_deferred_types(db, definition));
}
assert!(
self.types.deferred.is_empty(),
Expand Down Expand Up @@ -6394,6 +6399,7 @@ mod tests {
use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::DbWithTestSystem;
use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run};
use ruff_python_ast::PythonVersion;

use super::*;

Expand Down Expand Up @@ -6725,7 +6731,7 @@ mod tests {
fn dependency_implicit_instance_attribute() -> anyhow::Result<()> {
fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
let file_main = system_path_to_file(db, "/src/main.py").unwrap();
let ast = parsed_module(db, file_main);
let ast = parsed_module(db, file_main, PythonVersion::default());
// Get the second statement in `main.py` (x = …) and extract the expression
// node on the right-hand side:
let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value;
Expand Down Expand Up @@ -6808,7 +6814,7 @@ mod tests {
fn dependency_own_instance_member() -> anyhow::Result<()> {
fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
let file_main = system_path_to_file(db, "/src/main.py").unwrap();
let ast = parsed_module(db, file_main);
let ast = parsed_module(db, file_main, PythonVersion::default());
// Get the second statement in `main.py` (x = …) and extract the expression
// node on the right-hand side:
let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value;
Expand Down
3 changes: 2 additions & 1 deletion crates/red_knot_test/src/assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
//! ```

use crate::db::Db;
use red_knot_python_semantic::Program;
use regex::Regex;
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
Expand All @@ -58,7 +59,7 @@ impl InlineFileAssertions {
pub(crate) fn from_file(db: &Db, file: File) -> Self {
let source = source_text(db, file);
let lines = line_index(db, file);
let parsed = parsed_module(db, file);
let parsed = parsed_module(db, file, Program::get(db).python_version(db));
let comment_ranges = CommentRanges::from(parsed.tokens());
Self {
comment_ranges,
Expand Down
2 changes: 1 addition & 1 deletion crates/red_knot_test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ fn run_test(
let failures: Failures = test_files
.into_iter()
.filter_map(|test_file| {
let parsed = parsed_module(db, test_file.file);
let parsed = parsed_module(db, test_file.file, Program::get(db).python_version(db));

let mut diagnostics: Vec<Box<_>> = parsed
.errors()
Expand Down
1 change: 1 addition & 0 deletions crates/red_knot_wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ default = ["console_error_panic_hook"]

[dependencies]
red_knot_project = { workspace = true, default-features = false, features = ["deflate"] }
red_knot_python_semantic = { workspace = true }

ruff_db = { workspace = true, default-features = false, features = [] }
ruff_python_ast = { workspace = true }
Expand Down
Loading
Loading