mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
Limit single-input parser warning emission to the first logical input (#9304)
Keep later numeric warnings available after parser warning escalation. Preserve compound tokenizer warning emission and legacy malformed-source fallback. Assisted-by: OpenAI Codex:gpt-6-astra
This commit is contained in:
1 parent
e5554e23ab
commit
73b6806608
2 files changed
+91
-10
No files matched your search
@@ -1279,6 +1279,35 @@ fn first_parse_error(
|
||||
Err(error)
|
||||
}
|
||||
|
||||
/// The first logical input accepted by the single-input grammar.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum SingleModeStatementBoundary {
|
||||
Simple { newline_index: usize, end: TextSize },
|
||||
Compound { end: TextSize },
|
||||
}
|
||||
|
||||
/// Classify a logical input without changing parser diagnostics or recovery.
|
||||
#[must_use]
|
||||
pub fn single_mode_statement_boundary(
|
||||
parsed: &parser::Parsed<ast::Mod>,
|
||||
) -> Option<SingleModeStatementBoundary> {
|
||||
let ast::Mod::Module(module) = parsed.syntax() else {
|
||||
return None;
|
||||
};
|
||||
let first = module.body.first()?;
|
||||
if is_compound_stmt(first) {
|
||||
return Some(SingleModeStatementBoundary::Compound { end: first.end() });
|
||||
}
|
||||
let newline_index = parsed
|
||||
.tokens()
|
||||
.iter()
|
||||
.position(|token| token.kind() == TokenKind::Newline)?;
|
||||
Some(SingleModeStatementBoundary::Simple {
|
||||
newline_index,
|
||||
end: parsed.tokens()[newline_index].end(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Single mode reads only the first statement when it is a simple statement, so any code after
|
||||
/// its line is reported as multiple statements, ahead of errors the rest of the source has.
|
||||
#[must_use]
|
||||
@@ -1289,8 +1318,8 @@ pub fn single_mode_multiple_statements_error(
|
||||
let ast::Mod::Module(module) = parsed.syntax() else {
|
||||
return None;
|
||||
};
|
||||
let first = module.body.first()?;
|
||||
if is_compound_stmt(first) {
|
||||
let boundary = single_mode_statement_boundary(parsed)?;
|
||||
if matches!(boundary, SingleModeStatementBoundary::Compound { .. }) {
|
||||
// The interactive grammar ends a compound statement with a NEWLINE,
|
||||
// so the first token of a following statement is invalid syntax.
|
||||
let next = module.body.get(1)?;
|
||||
@@ -1315,9 +1344,9 @@ pub fn single_mode_multiple_statements_error(
|
||||
));
|
||||
}
|
||||
let tokens = parsed.tokens();
|
||||
let newline_index = tokens
|
||||
.iter()
|
||||
.position(|token| token.kind() == TokenKind::Newline)?;
|
||||
let SingleModeStatementBoundary::Simple { newline_index, .. } = boundary else {
|
||||
unreachable!()
|
||||
};
|
||||
let newline = &tokens[newline_index];
|
||||
if parsed
|
||||
.errors()
|
||||
|
||||
@@ -1738,6 +1738,7 @@ mod escape_warnings {
|
||||
warnings: Vec<ParserWarning>,
|
||||
tokenizer_recovery: bool,
|
||||
has_valid_syntax: bool,
|
||||
single_boundary: Option<compiler::SingleModeStatementBoundary>,
|
||||
}
|
||||
|
||||
fn collect_parser_warnings(
|
||||
@@ -1745,9 +1746,11 @@ mod escape_warnings {
|
||||
filename: &str,
|
||||
mode: Option<ruff_python_parser::Mode>,
|
||||
inherited_barry_as_flufl: bool,
|
||||
single_mode: bool,
|
||||
) -> CollectedParserWarnings {
|
||||
let mut ast_tokenizer_recovery = true;
|
||||
let mut has_valid_syntax = false;
|
||||
let mut single_boundary = None;
|
||||
let mut warnings = Vec::new();
|
||||
collect_numeric_literal_warnings(source, &mut warnings);
|
||||
// The compile that follows rejects this source; parsing it here
|
||||
@@ -1765,6 +1768,9 @@ mod escape_warnings {
|
||||
let parsed = ruff_python_parser::parse_unchecked(barry_source.source(), options);
|
||||
if parsed.has_valid_syntax() {
|
||||
has_valid_syntax = true;
|
||||
if single_mode {
|
||||
single_boundary = compiler::single_mode_statement_boundary(&parsed);
|
||||
}
|
||||
let mut visitor = EscapeWarningVisitor {
|
||||
source,
|
||||
warnings: &mut warnings,
|
||||
@@ -1837,6 +1843,7 @@ mod escape_warnings {
|
||||
warnings,
|
||||
tokenizer_recovery: ast_tokenizer_recovery,
|
||||
has_valid_syntax,
|
||||
single_boundary,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1861,14 +1868,20 @@ mod escape_warnings {
|
||||
inherited_barry_as_flufl: bool,
|
||||
emitted_escapes: &[usize],
|
||||
) -> Result<(), CompileWarningError> {
|
||||
let is_single = matches!(mode, compiler::Mode::Single);
|
||||
let mode = match mode {
|
||||
compiler::Mode::Eval => ruff_python_parser::Mode::Expression,
|
||||
compiler::Mode::Exec | compiler::Mode::Single | compiler::Mode::BlockExpr => {
|
||||
ruff_python_parser::Mode::Module
|
||||
}
|
||||
};
|
||||
let collected =
|
||||
collect_parser_warnings(source, filename, Some(mode), inherited_barry_as_flufl);
|
||||
let collected = collect_parser_warnings(
|
||||
source,
|
||||
filename,
|
||||
Some(mode),
|
||||
inherited_barry_as_flufl,
|
||||
is_single,
|
||||
);
|
||||
if !collected.has_valid_syntax || !collected.tokenizer_recovery {
|
||||
// Invalid source and interpolated tokenizer warnings need a
|
||||
// separate recovery contract. Preserve their existing behavior.
|
||||
@@ -1880,9 +1893,10 @@ mod escape_warnings {
|
||||
filename,
|
||||
module,
|
||||
collected.warnings,
|
||||
collected.single_boundary,
|
||||
WarningEmission::Standard {
|
||||
emitted_escapes,
|
||||
tokenizer_recovery: collected.tokenizer_recovery,
|
||||
tokenizer_recovery: true,
|
||||
},
|
||||
)
|
||||
.map(|_| ())
|
||||
@@ -1902,6 +1916,7 @@ mod escape_warnings {
|
||||
filename,
|
||||
module,
|
||||
warnings,
|
||||
None,
|
||||
WarningEmission::Standard {
|
||||
emitted_escapes: &[],
|
||||
tokenizer_recovery: false,
|
||||
@@ -1954,6 +1969,7 @@ mod escape_warnings {
|
||||
filename,
|
||||
module,
|
||||
warnings,
|
||||
None,
|
||||
WarningEmission::Standard {
|
||||
emitted_escapes,
|
||||
tokenizer_recovery: false,
|
||||
@@ -1975,13 +1991,26 @@ mod escape_warnings {
|
||||
CompileStart::Eval => Some(ruff_python_parser::Mode::Expression),
|
||||
CompileStart::FuncType => None,
|
||||
};
|
||||
let collected =
|
||||
collect_parser_warnings(source, filename, mode, inherited_barry_as_flufl);
|
||||
let collected = collect_parser_warnings(
|
||||
source,
|
||||
filename,
|
||||
mode,
|
||||
inherited_barry_as_flufl,
|
||||
matches!(start, CompileStart::Single),
|
||||
);
|
||||
self.emit_parser_warning_batch(
|
||||
source,
|
||||
filename,
|
||||
module.map(AsObject::as_object),
|
||||
collected.warnings,
|
||||
if matches!(start, CompileStart::Single)
|
||||
&& collected.has_valid_syntax
|
||||
&& collected.tokenizer_recovery
|
||||
{
|
||||
collected.single_boundary
|
||||
} else {
|
||||
None
|
||||
},
|
||||
WarningEmission::Ast {
|
||||
// Unlike an interactive prompt, Single compile input recovers
|
||||
// after a parser warning becomes an error. Later numeric
|
||||
@@ -2010,12 +2039,14 @@ mod escape_warnings {
|
||||
filename,
|
||||
Some(ruff_python_parser::Mode::Module),
|
||||
inherited_barry_as_flufl,
|
||||
false,
|
||||
);
|
||||
self.emit_parser_warning_batch(
|
||||
source,
|
||||
filename,
|
||||
module.map(AsObject::as_object),
|
||||
collected.warnings,
|
||||
None,
|
||||
WarningEmission::Interactive(emitted),
|
||||
)
|
||||
.map(|_| ())
|
||||
@@ -2027,6 +2058,7 @@ mod escape_warnings {
|
||||
filename: &str,
|
||||
module: Option<&PyObject>,
|
||||
warnings: Vec<ParserWarning>,
|
||||
single_boundary: Option<compiler::SingleModeStatementBoundary>,
|
||||
emission: WarningEmission<'_>,
|
||||
) -> Result<Vec<usize>, CompileWarningError> {
|
||||
let (emitted_escapes, ast_tokenizer_recovery, mut interactive) = match emission {
|
||||
@@ -2040,6 +2072,26 @@ mod escape_warnings {
|
||||
let mut considered_escapes = Vec::new();
|
||||
let mut parser_error = None;
|
||||
for warning in warnings {
|
||||
// A successful single input does not parse later statements.
|
||||
// Keep those numeric events for recovery after an earlier
|
||||
// parser warning escalates, rather than truncating the source.
|
||||
if parser_error.is_none()
|
||||
&& let Some(boundary) = single_boundary
|
||||
{
|
||||
let end = match (boundary, &warning) {
|
||||
(compiler::SingleModeStatementBoundary::Simple { end, .. }, _) => Some(end),
|
||||
(
|
||||
compiler::SingleModeStatementBoundary::Compound { end },
|
||||
ParserWarning::Escape(_, EscapeOrigin::Parser, _),
|
||||
) => Some(end),
|
||||
// Compound input requests another token and can emit
|
||||
// tokenizer warnings while rejecting the next statement.
|
||||
_ => None,
|
||||
};
|
||||
if end.is_some_and(|end| warning.offset() >= end.to_usize()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// CPython resumes tokenization after a parser-origin SyntaxError.
|
||||
// Preserve ordinary warning order and never replay prior warnings.
|
||||
if parser_error.is_some() && !matches!(warning, ParserWarning::Numeric { .. }) {
|
||||
|
||||
Reference in new issue
Block a user