Skip to content

Commit cf26775

Browse files
ironcevJoshuaBatty
andauthored
Improve warnings in match expressions (#4965)
## Description At the moment, we have just one warning message in `match` expressions: "This match arm is unreachable." This PR: - enriches the existing warning with more contextual information. - gives specific warning for redundant catch-all last arm as proposed in #3949. - gives specific warning for catch-all arm preceding other arms; this can easily happen by chance in more complex patterns, e.g., when confusing variable `A` for enum variant `Enum::A`. What we still want to do is give a hint to user when `A` is (very likely) used instead of `Enum::A`. This will be a part of a separate PR. The structure of the messages still follow the Elm approach as explained in the [Expressive Diagnostics RFC](FuelLabs/sway-rfcs#30 (comment)). Removing the _issue_ from the title and simplifying the wording will be a part of a different PR. Closes #3949. ## Demo ![Unreachable middle arm](https://github.com/FuelLabs/sway/assets/4142833/80a5c471-0995-45ec-96c4-ec43b9c09a9c) ![Unreachable last arm](https://github.com/FuelLabs/sway/assets/4142833/7fdd5e2e-f195-49b6-8042-f17b82164c96) ![Interior catch-all arm makes arms below it unreachable](https://github.com/FuelLabs/sway/assets/4142833/59d33c95-2409-48e9-8f12-d4e446997fa9) ![Redundant last catch-all arm](https://github.com/FuelLabs/sway/assets/4142833/e1bbea08-e343-4fb1-a357-56123e05046d) ## Checklist - [x] I have linked to any relevant issues. - [x] I have commented my code, particularly in hard-to-understand areas. - [ ] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [x] I have added tests that prove my fix is effective or that my feature works. - [ ] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [x] I have done my best to ensure that my PR adheres to [the Fuel Labs Code Review Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md). - [x] I have requested a review from the relevant team or maintainers. --------- Co-authored-by: Joshua Batty <joshpbatty@gmail.com>
1 parent 0213ce8 commit cf26775

File tree

27 files changed

+1968
-44
lines changed

27 files changed

+1968
-44
lines changed

sway-core/src/semantic_analysis/ast_node/expression/match_expression/analysis/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ mod reachable_report;
77
mod usefulness;
88
mod witness_report;
99

10+
pub(in crate::semantic_analysis::ast_node::expression) use reachable_report::ReachableReport;
1011
pub(crate) use usefulness::check_match_expression_usefulness;
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
use sway_types::Span;
2-
31
use crate::language::ty;
42

53
pub(crate) struct ReachableReport {
64
pub(crate) reachable: bool,
7-
pub(crate) span: Span,
5+
pub(crate) scrutinee: ty::TyScrutinee,
86
}
97

108
impl ReachableReport {
119
pub(super) fn new(reachable: bool, scrutinee: ty::TyScrutinee) -> ReachableReport {
1210
ReachableReport {
1311
reachable,
14-
span: scrutinee.span,
12+
scrutinee,
1513
}
1614
}
1715
}

sway-core/src/semantic_analysis/ast_node/expression/match_expression/typed/matcher.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub(crate) type MatcherResult = (MatchReqMap, MatchDeclMap);
2828
/// This algorithm desugars pattern matching into a [MatcherResult], by creating two lists,
2929
/// the [MatchReqMap] which is a list of requirements that a desugared if expression
3030
/// must include in the conditional in conjunctive normal form.
31-
/// and the [MatchImplMap] which is a list of variable
31+
/// and the [MatchDeclMap] which is a list of variable
3232
/// declarations that must be placed inside the body of the if expression.
3333
///
3434
/// Given the following example
@@ -62,7 +62,7 @@ pub(crate) type MatcherResult = (MatchReqMap, MatchDeclMap);
6262
/// ]
6363
/// ```
6464
///
65-
/// The first match arm would create a [MatchImplMap] of roughly:
65+
/// The first match arm would create a [MatchDeclMap] of roughly:
6666
///
6767
/// ```ignore
6868
/// [
@@ -81,7 +81,7 @@ pub(crate) type MatcherResult = (MatchReqMap, MatchDeclMap);
8181
/// ]
8282
/// ```
8383
///
84-
/// The second match arm would create a [MatchImplMap] of roughly:
84+
/// The second match arm would create a [MatchDeclMap] of roughly:
8585
///
8686
/// ```ignore
8787
/// [
@@ -103,7 +103,7 @@ pub(crate) type MatcherResult = (MatchReqMap, MatchDeclMap);
103103
/// ]
104104
/// ```
105105
///
106-
/// The third match arm would create a [MatchImplMap] of roughly:
106+
/// The third match arm would create a [MatchDeclMap] of roughly:
107107
///
108108
/// ```ignore
109109
/// []

sway-core/src/semantic_analysis/ast_node/expression/match_expression/typed/typed_scrutinee.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,26 @@ impl ty::TyScrutinee {
104104
Scrutinee::Error { err, .. } => Err(err),
105105
}
106106
}
107+
108+
/// Returns true if the `TyScrutinee` consists only of catch-all scrutinee variants, recursively.
109+
/// Catch-all variants are .., _, and variables.
110+
/// E.g.: (_, x, Point { .. })
111+
/// A catch-all scrutinee matches all the values of the corresponding type.
112+
pub(crate) fn is_catch_all(&self) -> bool {
113+
match &self.variant {
114+
ty::TyScrutineeVariant::CatchAll => true,
115+
ty::TyScrutineeVariant::Variable(_) => true,
116+
ty::TyScrutineeVariant::Literal(_) => false,
117+
ty::TyScrutineeVariant::Constant { .. } => false,
118+
ty::TyScrutineeVariant::StructScrutinee { fields, .. } => fields
119+
.iter()
120+
.filter_map(|x| x.scrutinee.as_ref())
121+
.all(|x| x.is_catch_all()),
122+
ty::TyScrutineeVariant::Or(elems) => elems.iter().all(|x| x.is_catch_all()),
123+
ty::TyScrutineeVariant::Tuple(elems) => elems.iter().all(|x| x.is_catch_all()),
124+
ty::TyScrutineeVariant::EnumScrutinee { .. } => false,
125+
}
126+
}
107127
}
108128

109129
fn type_check_variable(

sway-core/src/semantic_analysis/ast_node/expression/typed_expression.rs

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod tuple_index_access;
1010
mod unsafe_downcast;
1111

1212
use self::constant_expression::instantiate_constant_expression;
13+
1314
pub(crate) use self::{
1415
enum_instantiation::*, function_application::*, if_expression::*, lazy_operator::*,
1516
method_application::*, struct_field_access::*, struct_instantiation::*, tuple_index_access::*,
@@ -24,7 +25,7 @@ use crate::{
2425
ty::{self, TyImplItem},
2526
*,
2627
},
27-
semantic_analysis::*,
28+
semantic_analysis::{expression::ReachableReport, *},
2829
transform::to_parsed_lang::type_name_to_type_info_opt,
2930
type_system::*,
3031
Engines,
@@ -646,14 +647,69 @@ impl ty::TyExpression {
646647
typed_scrutinees.clone(),
647648
span.clone(),
648649
)?;
649-
for reachable_report in arms_reachability.into_iter() {
650-
if !reachable_report.reachable {
650+
651+
// if there is an interior catch-all arm
652+
if let Some(catch_all_arm_position) = interior_catch_all_arm_position(&arms_reachability) {
653+
// show the warning on the arms below it that it makes them unreachable...
654+
for reachable_report in arms_reachability[catch_all_arm_position + 1..].iter() {
651655
handler.emit_warn(CompileWarning {
652-
span: reachable_report.span,
653-
warning_content: Warning::MatchExpressionUnreachableArm,
656+
span: reachable_report.scrutinee.span.clone(),
657+
warning_content: Warning::MatchExpressionUnreachableArm {
658+
match_value: value.span(),
659+
preceding_arms: arms_reachability[catch_all_arm_position]
660+
.scrutinee
661+
.span
662+
.clone(),
663+
preceding_arm_is_catch_all: true,
664+
unreachable_arm: reachable_report.scrutinee.span.clone(),
665+
// In this case id doesn't matter if the concrete unreachable arm is
666+
// the last arm or a catch-all arm itself.
667+
// We want to point out the interior catch-all arm as problematic.
668+
// So we simply put these two values both to false.
669+
is_last_arm: false,
670+
is_catch_all_arm: false,
671+
},
654672
});
655673
}
674+
675+
//...but still check the arms above it for reachability
676+
check_interior_non_catch_all_arms_for_reachability(
677+
handler,
678+
&value.span(),
679+
&arms_reachability[..catch_all_arm_position],
680+
);
656681
}
682+
// if there are no interior catch-all arms and there is more then one arm
683+
else if let Some((last_arm_report, other_arms_reachability)) =
684+
arms_reachability.split_last()
685+
{
686+
// check reachable report for all the arms except the last one
687+
check_interior_non_catch_all_arms_for_reachability(
688+
handler,
689+
&value.span(),
690+
other_arms_reachability,
691+
);
692+
693+
// for the last one, give a different warning if it is an unreachable catch-all arm
694+
if !last_arm_report.reachable {
695+
handler.emit_warn(CompileWarning {
696+
span: last_arm_report.scrutinee.span.clone(),
697+
warning_content: Warning::MatchExpressionUnreachableArm {
698+
match_value: value.span(),
699+
preceding_arms: Span::join_all(
700+
other_arms_reachability
701+
.iter()
702+
.map(|report| report.scrutinee.span.clone()),
703+
),
704+
preceding_arm_is_catch_all: false,
705+
unreachable_arm: last_arm_report.scrutinee.span.clone(),
706+
is_last_arm: true,
707+
is_catch_all_arm: last_arm_report.scrutinee.is_catch_all(),
708+
},
709+
});
710+
}
711+
}
712+
657713
if witness_report.has_witnesses() {
658714
return Err(
659715
handler.emit_err(CompileError::MatchExpressionNonExhaustive {
@@ -675,7 +731,46 @@ impl ty::TyExpression {
675731
},
676732
};
677733

678-
Ok(match_exp)
734+
return Ok(match_exp);
735+
736+
/// Returns the position of the first match arm that is an "interior" arm, meaning:
737+
/// - arm is a catch-all arm
738+
/// - arm is not the last match arm
739+
/// or `None` if such arm does not exist.
740+
/// Note that the arm can be the first arm.
741+
fn interior_catch_all_arm_position(arms_reachability: &[ReachableReport]) -> Option<usize> {
742+
arms_reachability
743+
.split_last()?
744+
.1
745+
.iter()
746+
.position(|report| report.scrutinee.is_catch_all())
747+
}
748+
749+
fn check_interior_non_catch_all_arms_for_reachability(
750+
handler: &Handler,
751+
match_value: &Span,
752+
arms_reachability: &[ReachableReport],
753+
) {
754+
for (index, reachable_report) in arms_reachability.iter().enumerate() {
755+
if !reachable_report.reachable {
756+
handler.emit_warn(CompileWarning {
757+
span: reachable_report.scrutinee.span.clone(),
758+
warning_content: Warning::MatchExpressionUnreachableArm {
759+
match_value: match_value.clone(),
760+
preceding_arms: Span::join_all(
761+
arms_reachability[..index]
762+
.iter()
763+
.map(|report| report.scrutinee.span.clone()),
764+
),
765+
preceding_arm_is_catch_all: false,
766+
unreachable_arm: reachable_report.scrutinee.span.clone(),
767+
is_last_arm: false,
768+
is_catch_all_arm: false,
769+
},
770+
});
771+
}
772+
}
773+
}
679774
}
680775

681776
#[allow(clippy::too_many_arguments)]

sway-error/src/diagnostic.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,22 @@ impl Diagnostic {
8282
pub fn related_sources(&self, include_issue_source: bool) -> Vec<&SourcePath> {
8383
let mut source_files = vec![];
8484

85-
// All unwrappings are safe because we check the existence
86-
// either in is_in_source() or in in_source_info().
87-
if self.issue.is_in_source() && include_issue_source {
85+
let issue_is_in_source = self.issue.is_in_source();
86+
87+
// All source_path() unwrappings are safe because we check the existence
88+
// of source in case of issue, and self.labels() returns
89+
// only labels that are in source.
90+
if issue_is_in_source && include_issue_source {
8891
source_files.push(self.issue.source_path().unwrap());
8992
}
9093

9194
for hint in self.labels() {
9295
let file = hint.source_path().unwrap();
9396

94-
if !include_issue_source && file == self.issue.source_path().unwrap() {
97+
if !include_issue_source
98+
&& issue_is_in_source
99+
&& file == self.issue.source_path().unwrap()
100+
{
95101
continue;
96102
}
97103

sway-error/src/error.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -910,7 +910,7 @@ impl ToDiagnostic for CompileError {
910910
),
911911
],
912912
help: vec![
913-
"Unlike variables, constants cannot be shadowed by other constants or variables.".to_string(),
913+
format!("Unlike variables, constants cannot be shadowed by other constants or variables."),
914914
match (variable_or_constant.as_str(), constant_decl.clone() != Span::dummy()) {
915915
("Variable", false) => format!("Consider renaming either the variable \"{name}\" or the constant \"{name}\"."),
916916
("Constant", false) => "Consider renaming one of the constants.".to_string(),
@@ -924,7 +924,7 @@ impl ToDiagnostic for CompileError {
924924
],
925925
},
926926
ConstantShadowsVariable { name , variable_span } => Diagnostic {
927-
reason: Some(Reason::new(code(2), "Constants cannot shadow variables".to_string())),
927+
reason: Some(Reason::new(code(1), "Constants cannot shadow variables".to_string())),
928928
issue: Issue::error(
929929
source_engine,
930930
name.span(),
@@ -943,8 +943,8 @@ impl ToDiagnostic for CompileError {
943943
),
944944
],
945945
help: vec![
946-
"Variables can shadow other variables, but constants cannot.".to_string(),
947-
"Consider renaming either the variable or the constant.".to_string(),
946+
format!("Variables can shadow other variables, but constants cannot."),
947+
format!("Consider renaming either the variable or the constant."),
948948
],
949949
},
950950
_ => Diagnostic {

sway-error/src/warning.rs

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,14 @@ pub enum Warning {
8484
DeadStorageDeclarationForFunction {
8585
unneeded_attrib: String,
8686
},
87-
MatchExpressionUnreachableArm,
87+
MatchExpressionUnreachableArm {
88+
match_value: Span,
89+
preceding_arms: Span,
90+
preceding_arm_is_catch_all: bool,
91+
unreachable_arm: Span,
92+
is_last_arm: bool,
93+
is_catch_all_arm: bool,
94+
},
8895
UnrecognizedAttribute {
8996
attrib_name: Ident,
9097
},
@@ -217,7 +224,7 @@ impl fmt::Display for Warning {
217224
"This function's storage attributes declaration does not match its \
218225
actual storage access pattern: '{unneeded_attrib}' attribute(s) can be removed."
219226
),
220-
MatchExpressionUnreachableArm => write!(f, "This match arm is unreachable."),
227+
MatchExpressionUnreachableArm { .. } => write!(f, "This match arm is unreachable."),
221228
UnrecognizedAttribute {attrib_name} => write!(f, "Unknown attribute: \"{attrib_name}\"."),
222229
AttributeExpectedNumberOfArguments {attrib_name, received_args, expected_min_len, expected_max_len } => write!(
223230
f,
@@ -258,21 +265,83 @@ impl ToDiagnostic for CompileWarning {
258265
issue: Issue::warning(
259266
source_engine,
260267
name.span(),
261-
format!("Constant \"{name}\" should be SCREAMING_SNAKE_CASE")
268+
format!("Constant \"{name}\" should be SCREAMING_SNAKE_CASE"),
262269
),
263270
hints: vec![
264271
Hint::warning(
265272
source_engine,
266273
name.span(),
267-
format!("\"{name}\" should be SCREAMING_SNAKE_CASE, like \"{}\".", to_screaming_snake_case(name.as_str()))
274+
format!("\"{name}\" should be SCREAMING_SNAKE_CASE, like \"{}\".", to_screaming_snake_case(name.as_str())),
268275
),
269276
],
270277
help: vec![
271-
"In Sway, ABIs, structs, traits, and enums are CapitalCase.".to_string(),
272-
"Modules, variables, and functions are snake_case, while constants are SCREAMING_SNAKE_CASE.".to_string(),
278+
format!("In Sway, ABIs, structs, traits, and enums are CapitalCase."),
279+
format!("Modules, variables, and functions are snake_case, while constants are SCREAMING_SNAKE_CASE."),
273280
format!("Consider renaming the constant to, e.g., \"{}\".", to_screaming_snake_case(name.as_str())),
274281
],
275282
},
283+
MatchExpressionUnreachableArm { match_value, preceding_arms, preceding_arm_is_catch_all, unreachable_arm, is_last_arm, is_catch_all_arm } => Diagnostic {
284+
reason: Some(Reason::new(code(1), "Match arm is unreachable".to_string())),
285+
issue: Issue::warning(
286+
source_engine,
287+
unreachable_arm.clone(),
288+
match (*is_last_arm, *is_catch_all_arm) {
289+
(true, true) => format!("Catch-all pattern \"{}\" in the last match arm will never be matched", unreachable_arm.as_str()),
290+
_ => format!("Pattern \"{}\" will never be matched", unreachable_arm.as_str())
291+
}
292+
),
293+
hints: vec![
294+
Hint::warning(
295+
source_engine,
296+
unreachable_arm.clone(),
297+
format!("{} arm \"{}\" is unreachable.", if *is_last_arm && *is_catch_all_arm { "Last catch-all match" } else { "Match" }, unreachable_arm.as_str())
298+
),
299+
Hint::info(
300+
source_engine,
301+
match_value.clone(),
302+
"This is the value to match on.".to_string()
303+
),
304+
if *preceding_arm_is_catch_all {
305+
Hint::warning(
306+
source_engine,
307+
preceding_arms.clone(),
308+
format!("Catch-all arm \"{}\" makes all the arms below it unreachable.", preceding_arms.as_str())
309+
)
310+
}
311+
else {
312+
Hint::info(
313+
source_engine,
314+
preceding_arms.clone(),
315+
if *is_last_arm {
316+
format!("Preceding match arms already match all possible values of \"{}\".", match_value.as_str())
317+
}
318+
else {
319+
format!("Preceding match arms already match all the values that \"{}\" can match.", unreachable_arm.as_str())
320+
}
321+
)
322+
}
323+
],
324+
help: if *preceding_arm_is_catch_all {
325+
vec![
326+
format!("Catch-all patterns make sense only in last match arms."),
327+
format!("Carefully check matching logic in all the arms and consider:"),
328+
format!(" - removing the catch-all arm \"{}\" or making it the last arm.", preceding_arms.as_str()),
329+
format!(" - removing the unreachable arms below \"{}\".", preceding_arms.as_str()),
330+
]
331+
}
332+
else if *is_last_arm && *is_catch_all_arm {
333+
vec![
334+
format!("Catch-all patterns are often used in last match arms."),
335+
format!("But in this case, the preceding arms already match all possible values of \"{}\".", match_value.as_str()),
336+
format!("Carefully check matching logic in all the arms and consider removing the unreachable last catch-all arm."),
337+
]
338+
}
339+
else {
340+
vec![
341+
format!("Carefully check matching logic in all the arms and consider removing the unreachable arm."),
342+
]
343+
}
344+
},
276345
_ => Diagnostic {
277346
// TODO: Temporary we use self here to achieve backward compatibility.
278347
// In general, self must not be used and will not be used once we

0 commit comments

Comments
 (0)