Skip to content

Commit 9223880

Browse files
yangdanny97meta-codesync[bot]
authored andcommitted
error on break/continue outside of loop
Summary: This diff sets up the proper plumbing for Ruff's SemanticSyntaxChecker, which was being used incorrectly before. This lets us emit errors for break/continue outside of loops. Not all errors are emitted here because some are redundant w/ existing errors that Pyrefly emits, and to reduce the scope of changes in this diff. We can turn additional errors on in the future with appropriate testing. closes #1796 Reviewed By: grievejia Differential Revision: D88700199 fbshipit-source-id: ac7e2515af83f9fe3ae4dc260807a695712f989c
1 parent d1ffba5 commit 9223880

File tree

8 files changed

+241
-124
lines changed

8 files changed

+241
-124
lines changed

crates/pyrefly_config/src/error_kind.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ pub enum ErrorKind {
179179
/// e.g. calling `super(Y, x)` on an object `x` that does not match the class `Y`.
180180
InvalidSuperCall,
181181
/// Incorrect Python syntax, construct is not allowed in this position.
182-
/// In many cases a syntax error will also be reported.
182+
/// In many cases a parse error will also be reported.
183183
InvalidSyntax,
184184
/// An error related to type alias usage or definition.
185185
InvalidTypeAlias,

pyrefly/lib/binding/bindings.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
use std::cell::RefCell;
89
use std::fmt;
910
use std::fmt::Debug;
1011
use std::fmt::Display;
@@ -30,6 +31,10 @@ use ruff_python_ast::Stmt;
3031
use ruff_python_ast::TypeParam;
3132
use ruff_python_ast::TypeParams;
3233
use ruff_python_ast::name::Name;
34+
use ruff_python_parser::semantic_errors::SemanticSyntaxChecker;
35+
use ruff_python_parser::semantic_errors::SemanticSyntaxContext;
36+
use ruff_python_parser::semantic_errors::SemanticSyntaxError;
37+
use ruff_python_parser::semantic_errors::SemanticSyntaxErrorKind;
3338
use ruff_text_size::Ranged;
3439
use ruff_text_size::TextRange;
3540
use ruff_text_size::TextSize;
@@ -197,6 +202,8 @@ pub struct BindingsBuilder<'a> {
197202
unused_parameters: Vec<UnusedParameter>,
198203
unused_imports: Vec<UnusedImport>,
199204
unused_variables: Vec<UnusedVariable>,
205+
semantic_checker: SemanticSyntaxChecker,
206+
semantic_syntax_errors: RefCell<Vec<SemanticSyntaxError>>,
200207
}
201208

202209
/// An enum tracking whether we are in a generator expression
@@ -402,6 +409,8 @@ impl Bindings {
402409
unused_parameters: Vec::new(),
403410
unused_imports: Vec::new(),
404411
unused_variables: Vec::new(),
412+
semantic_checker: SemanticSyntaxChecker::new(),
413+
semantic_syntax_errors: RefCell::new(Vec::new()),
405414
};
406415
builder.init_static_scope(&x.body, true);
407416
if module_info.name() != ModuleName::builtins() {
@@ -426,6 +435,18 @@ impl Bindings {
426435
let unused_imports = builder.scopes.collect_module_unused_imports();
427436
builder.record_unused_imports(unused_imports);
428437
let scope_trace = builder.scopes.finish();
438+
439+
let semantic_errors = builder.semantic_syntax_errors.into_inner();
440+
for error in semantic_errors {
441+
if Self::should_emit_semantic_syntax_error(&error) {
442+
builder.errors.add(
443+
error.range,
444+
ErrorInfo::Kind(ErrorKind::InvalidSyntax),
445+
vec1![error.to_string()],
446+
);
447+
}
448+
}
449+
429450
let exported = exports.exports(lookup);
430451
for (name, exportable) in scope_trace.exportables().into_iter_hashed() {
431452
let binding = match exportable {
@@ -456,6 +477,43 @@ impl Bindings {
456477
unused_variables: builder.unused_variables,
457478
}))
458479
}
480+
481+
fn should_emit_semantic_syntax_error(error: &SemanticSyntaxError) -> bool {
482+
// TODO: enable and add tests for the other semantic syntax errors
483+
match error.kind {
484+
SemanticSyntaxErrorKind::BreakOutsideLoop
485+
| SemanticSyntaxErrorKind::ContinueOutsideLoop
486+
| SemanticSyntaxErrorKind::SingleStarredAssignment
487+
| SemanticSyntaxErrorKind::DifferentMatchPatternBindings
488+
| SemanticSyntaxErrorKind::IrrefutableCasePattern(_) => true,
489+
SemanticSyntaxErrorKind::LateFutureImport
490+
| SemanticSyntaxErrorKind::InvalidStarExpression
491+
| SemanticSyntaxErrorKind::ReboundComprehensionVariable
492+
| SemanticSyntaxErrorKind::DuplicateTypeParameter
493+
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
494+
| SemanticSyntaxErrorKind::WriteToDebug(_)
495+
| SemanticSyntaxErrorKind::InvalidExpression(_, _)
496+
| SemanticSyntaxErrorKind::DuplicateMatchKey(_)
497+
| SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_)
498+
| SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { .. }
499+
| SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. }
500+
| SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
501+
| SemanticSyntaxErrorKind::YieldOutsideFunction(_)
502+
| SemanticSyntaxErrorKind::ReturnOutsideFunction
503+
| SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(_)
504+
| SemanticSyntaxErrorKind::DuplicateParameter(_)
505+
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel
506+
| SemanticSyntaxErrorKind::NonlocalAndGlobal(_)
507+
| SemanticSyntaxErrorKind::AnnotatedGlobal(_)
508+
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_)
509+
| SemanticSyntaxErrorKind::YieldFromInAsyncFunction
510+
| SemanticSyntaxErrorKind::NonModuleImportStar(_)
511+
| SemanticSyntaxErrorKind::MultipleStarredExpressions
512+
| SemanticSyntaxErrorKind::FutureFeatureNotDefined(_)
513+
| SemanticSyntaxErrorKind::GlobalParameter(_)
514+
| SemanticSyntaxErrorKind::NonlocalWithoutBinding(_) => false,
515+
}
516+
}
459517
}
460518

461519
impl BindingTable {
@@ -1467,4 +1525,81 @@ impl<'a> BindingsBuilder<'a> {
14671525
}
14681526
}
14691527
}
1528+
1529+
pub fn with_semantic_checker(&mut self, f: impl FnOnce(&mut SemanticSyntaxChecker, &Self)) {
1530+
let mut checker = std::mem::take(&mut self.semantic_checker);
1531+
f(&mut checker, self);
1532+
self.semantic_checker = checker;
1533+
}
1534+
}
1535+
1536+
impl<'a> SemanticSyntaxContext for BindingsBuilder<'a> {
1537+
fn python_version(&self) -> ruff_python_ast::PythonVersion {
1538+
ruff_python_ast::PythonVersion {
1539+
major: self.sys_info.version().major as u8,
1540+
minor: self.sys_info.version().minor as u8,
1541+
}
1542+
}
1543+
1544+
fn source(&self) -> &str {
1545+
self.module_info.contents()
1546+
}
1547+
1548+
fn future_annotations_or_stub(&self) -> bool {
1549+
self.module_info.source_type() == ruff_python_ast::PySourceType::Stub
1550+
}
1551+
1552+
fn report_semantic_error(&self, error: SemanticSyntaxError) {
1553+
self.semantic_syntax_errors.borrow_mut().push(error);
1554+
}
1555+
1556+
fn global(&self, name: &str) -> Option<TextRange> {
1557+
self.scopes.get_global_declaration(name)
1558+
}
1559+
1560+
fn has_nonlocal_binding(&self, name: &str) -> bool {
1561+
self.scopes.has_nonlocal_binding(name)
1562+
}
1563+
1564+
fn in_async_context(&self) -> bool {
1565+
self.scopes.is_in_async_def()
1566+
}
1567+
1568+
fn in_await_allowed_context(&self) -> bool {
1569+
// await is allowed in functions, lambdas, and notebooks
1570+
self.scopes.in_function_scope() || self.in_notebook()
1571+
}
1572+
1573+
fn in_yield_allowed_context(&self) -> bool {
1574+
// yield is allowed in functions and lambdas, but not in comprehensions or classes
1575+
self.scopes.in_function_scope()
1576+
}
1577+
1578+
fn in_sync_comprehension(&self) -> bool {
1579+
self.scopes.in_sync_comprehension()
1580+
}
1581+
1582+
fn in_module_scope(&self) -> bool {
1583+
self.scopes.in_module_or_class_top_level() && !self.scopes.in_class_body()
1584+
}
1585+
1586+
fn in_function_scope(&self) -> bool {
1587+
self.scopes.in_function_scope()
1588+
}
1589+
1590+
fn in_generator_scope(&self) -> bool {
1591+
self.scopes.in_generator_expression()
1592+
}
1593+
1594+
fn in_notebook(&self) -> bool {
1595+
self.module_info.source_type() == ruff_python_ast::PySourceType::Ipynb
1596+
}
1597+
1598+
fn in_loop_context(&self) -> bool {
1599+
self.scopes.loop_depth() > 0
1600+
}
1601+
1602+
fn is_bound_parameter(&self, name: &str) -> bool {
1603+
self.scopes.is_bound_parameter(name)
1604+
}
14701605
}

pyrefly/lib/binding/expr.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ impl<'a> BindingsBuilder<'a> {
399399
"`async` can only be used inside an async function".to_owned(),
400400
);
401401
}
402-
self.scopes.push(Scope::comprehension(range));
402+
self.scopes.push(Scope::comprehension(range, is_generator));
403403
}
404404
// Incomplete nested comprehensions can have identical iterators
405405
// for inner and outer loops. It is safe to overwrite it because it literally the same.
@@ -506,6 +506,8 @@ impl<'a> BindingsBuilder<'a> {
506506

507507
/// Execute through the expr, ensuring every name has a binding.
508508
pub fn ensure_expr(&mut self, x: &mut Expr, usage: &mut Usage) {
509+
self.with_semantic_checker(|semantic, context| semantic.visit_expr(x, context));
510+
509511
match x {
510512
Expr::If(x) => {
511513
// Ternary operation. We treat it like an if/else statement.

pyrefly/lib/binding/scope.rs

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,7 @@ impl ScopeMethod {
866866
enum ScopeKind {
867867
Annotation,
868868
Class(ScopeClass),
869-
Comprehension,
869+
Comprehension { is_generator: bool },
870870
Function(ScopeFunction),
871871
Method(ScopeMethod),
872872
Module,
@@ -989,11 +989,11 @@ impl Scope {
989989
)
990990
}
991991

992-
pub fn comprehension(range: TextRange) -> Self {
992+
pub fn comprehension(range: TextRange, is_generator: bool) -> Self {
993993
Self::new(
994994
range,
995995
FlowBarrier::AllowFlowChecked,
996-
ScopeKind::Comprehension,
996+
ScopeKind::Comprehension { is_generator },
997997
)
998998
}
999999

@@ -1417,6 +1417,90 @@ impl Scopes {
14171417
self.current().loops.len()
14181418
}
14191419

1420+
/// Check if a name is declared as global in the current scope.
1421+
/// Returns the range of the global statement if found.
1422+
pub fn get_global_declaration(&self, name: &str) -> Option<TextRange> {
1423+
if let Some(static_info) = self.current().stat.0.get(&Name::new(name))
1424+
&& let StaticStyle::MutableCapture(MutableCapture {
1425+
kind: MutableCaptureKind::Global,
1426+
..
1427+
}) = &static_info.style
1428+
{
1429+
return Some(static_info.range);
1430+
}
1431+
None
1432+
}
1433+
1434+
/// Check if a name has a nonlocal binding in an enclosing scope.
1435+
pub fn has_nonlocal_binding(&self, name: &str) -> bool {
1436+
let name_obj = Name::new(name);
1437+
// Skip the current scope and check enclosing scopes
1438+
for scope in self.iter_rev().skip(1) {
1439+
// Check if this name is defined in this scope (not as a mutable capture)
1440+
if let Some(static_info) = scope.stat.0.get(&name_obj) {
1441+
match &static_info.style {
1442+
// Don't count mutable captures as nonlocal bindings
1443+
StaticStyle::MutableCapture(..) => continue,
1444+
// Any other definition counts as a binding
1445+
_ => return true,
1446+
}
1447+
}
1448+
}
1449+
false
1450+
}
1451+
1452+
/// Check if we're currently in a comprehension scope (not a generator expression).
1453+
pub fn in_comprehension(&self) -> bool {
1454+
matches!(self.current().kind, ScopeKind::Comprehension { .. })
1455+
}
1456+
1457+
/// Check if we're in a synchronous comprehension.
1458+
/// A comprehension is synchronous unless we're in an async function.
1459+
pub fn in_sync_comprehension(&self) -> bool {
1460+
if !self.in_comprehension() {
1461+
return false;
1462+
}
1463+
// Check if any enclosing scope is an async function
1464+
for scope in self.iter_rev().skip(1) {
1465+
if let ScopeKind::Function(func_scope) = &scope.kind {
1466+
return !func_scope.is_async;
1467+
} else if let ScopeKind::Method(method_scope) = &scope.kind {
1468+
return !method_scope.is_async;
1469+
}
1470+
}
1471+
// If we didn't find a function, it's synchronous
1472+
true
1473+
}
1474+
1475+
/// Check if we're in a generator expression scope.
1476+
/// Generator expressions are created for `Expr::Generator` comprehensions.
1477+
pub fn in_generator_expression(&self) -> bool {
1478+
matches!(
1479+
self.current().kind,
1480+
ScopeKind::Comprehension { is_generator: true }
1481+
)
1482+
}
1483+
1484+
/// Check if a name is a bound parameter in the current function scope.
1485+
pub fn is_bound_parameter(&self, name: &str) -> bool {
1486+
let name_obj = Name::new(name);
1487+
// Check the current scope and enclosing scopes for a function with this parameter
1488+
for scope in self.iter_rev() {
1489+
match &scope.kind {
1490+
ScopeKind::Function(func_scope) => {
1491+
return func_scope.parameters.contains_key(&name_obj);
1492+
}
1493+
ScopeKind::Method(method_scope) => {
1494+
return method_scope.parameters.contains_key(&name_obj);
1495+
}
1496+
// Don't look past module or class boundaries
1497+
ScopeKind::Module | ScopeKind::Class(_) => return false,
1498+
_ => {}
1499+
}
1500+
}
1501+
false
1502+
}
1503+
14201504
/// Track a narrow for a name in the current flow. This should result from options
14211505
/// that only narrow an existing value, not operations that assign a new value at runtime.
14221506
///

pyrefly/lib/binding/stmt.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ impl<'a> BindingsBuilder<'a> {
299299
/// Evaluate the statements and update the bindings.
300300
/// Every statement should end up in the bindings, perhaps with a location that is never used.
301301
pub fn stmt(&mut self, x: Stmt, parent: &NestingContext) {
302+
self.with_semantic_checker(|semantic, context| semantic.visit_stmt(&x, context));
303+
302304
match x {
303305
Stmt::FunctionDef(x) => {
304306
self.function_def(x, parent);

0 commit comments

Comments
 (0)