Skip to content

Commit aeb4cbc

Browse files
authored
Add lint warning for ambiguous if-statement followed by unary operator (#2759)
Because we support unary `+` and `-` a user can mistakenly write an if-statement followed by something like ` - x` and expect that it subtracts that from the result of the if-statement, when in fact parser treats this as two separate statements. This link helps call attention to the ambiguity and encourage the user to use clearer syntax to disambiguate.
1 parent 8072819 commit aeb4cbc

File tree

3 files changed

+125
-3
lines changed

3 files changed

+125
-3
lines changed

source/compiler/qsc_linter/src/lints/hir.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
use qsc_data_structures::span::Span;
55
use qsc_hir::{
66
hir::{
7-
BinOp, CallableDecl, CallableKind, Expr, ExprKind, Field, ItemKind, Res, SpecBody,
8-
SpecDecl, Stmt, StmtKind,
7+
BinOp, Block, CallableDecl, CallableKind, Expr, ExprKind, Field, ItemKind, Res, SpecBody,
8+
SpecDecl, Stmt, StmtKind, UnOp,
99
},
1010
ty::{Prim, Ty},
1111
visit::{self, Visitor},
@@ -40,6 +40,7 @@ declare_hir_lints! {
4040
(DeprecatedFunctionConstructor, LintLevel::Allow, "deprecated function constructors", "function constructors for struct types are deprecated, use `new` instead"),
4141
(DeprecatedWithOperator, LintLevel::Allow, "deprecated `w/` and `w/=` operators for structs", "`w/` and `w/=` operators for structs are deprecated, use `new` instead"),
4242
(DeprecatedDoubleColonOperator, LintLevel::Allow, "deprecated `::` for field access", "`::` operator is deprecated, use `.` instead"),
43+
(AmbiguousUnaryOperatorAfterIf, LintLevel::Warn, "ambiguous unary operator after if-expression", "consider wrapping the if-expression in parentheses or using a semicolon to clarify the intended use of the operator"),
4344
}
4445

4546
#[derive(Default)]
@@ -342,3 +343,37 @@ impl HirLintPass for DeprecatedDoubleColonOperator {
342343
}
343344
}
344345
}
346+
347+
#[derive(Default)]
348+
struct AmbiguousUnaryOperatorAfterIf {
349+
level: LintLevel,
350+
}
351+
352+
/// Creates a lint for ambiguous unary operators after if-statements.
353+
/// For example:
354+
/// ```qsharp
355+
/// if condition { a } else { b } - c
356+
/// ```
357+
/// The user likely intended this to subtract `c` from the result of the if-expression,
358+
/// but to achieve this they need to wrap the if-expression in parentheses, like so:
359+
/// ```qsharp
360+
/// (if condition { a } else { b }) - c
361+
/// ```
362+
impl HirLintPass for AmbiguousUnaryOperatorAfterIf {
363+
fn check_block(&mut self, block: &Block, buffer: &mut Vec<Lint>, _compilation: Compilation) {
364+
if block.stmts.len() < 2 {
365+
// Not enough statements to have an if-expression followed by a unary operator.
366+
return;
367+
}
368+
for i in 0..(block.stmts.len() - 1) {
369+
if let StmtKind::Expr(expr) = &block.stmts[i].kind
370+
&& matches!(&expr.kind, ExprKind::If(..))
371+
&& let StmtKind::Expr(next_expr) = &block.stmts[i + 1].kind
372+
&& matches!(next_expr.kind, ExprKind::UnOp(UnOp::Pos | UnOp::Neg, _))
373+
&& expr.ty != Ty::UNIT
374+
{
375+
buffer.push(lint!(self, next_expr.span));
376+
}
377+
}
378+
}
379+
}

source/compiler/qsc_linter/src/tests.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,92 @@ fn deprecated_assign_update_expr_code_action() {
809809
);
810810
}
811811

812+
#[test]
813+
fn ambiguous_unary_operator_after_if() {
814+
check(
815+
&wrap_in_callable("if true { 42 } else { 0 } - 1", CallableKind::Function),
816+
&expect![[r#"
817+
[
818+
SrcLint {
819+
source: "- 1",
820+
level: Warn,
821+
message: "ambiguous unary operator after if-expression",
822+
help: "consider wrapping the if-expression in parentheses or using a semicolon to clarify the intended use of the operator",
823+
code_action_edits: [],
824+
},
825+
]
826+
"#]],
827+
);
828+
}
829+
830+
#[test]
831+
fn ambiguous_unary_operator_after_if_fixed_by_parens_around_if() {
832+
check(
833+
&wrap_in_callable("(if true { 42 } else { 0 }) - 1", CallableKind::Function),
834+
&expect![[r#"
835+
[]
836+
"#]],
837+
);
838+
}
839+
840+
#[test]
841+
fn ambiguous_unary_operator_after_if_fixed_by_parens_around_all() {
842+
check(
843+
&wrap_in_callable("(if true { 42 } else { 0 } - 1)", CallableKind::Function),
844+
&expect![[r#"
845+
[]
846+
"#]],
847+
);
848+
}
849+
850+
#[test]
851+
fn ambiguous_unary_operator_after_if_fixed_by_semicolon() {
852+
check(
853+
&wrap_in_callable("if true { 42 } else { 0 }; - 1", CallableKind::Function),
854+
&expect![[r#"
855+
[]
856+
"#]],
857+
);
858+
}
859+
860+
#[test]
861+
fn ambiguous_unary_operator_after_if_does_trigger_warning_on_different_non_unit_types() {
862+
check(
863+
&wrap_in_callable("if true { 42.0 } else { 0.0 } - 1", CallableKind::Function),
864+
&expect![[r#"
865+
[
866+
SrcLint {
867+
source: "- 1",
868+
level: Warn,
869+
message: "ambiguous unary operator after if-expression",
870+
help: "consider wrapping the if-expression in parentheses or using a semicolon to clarify the intended use of the operator",
871+
code_action_edits: [],
872+
},
873+
]
874+
"#]],
875+
);
876+
}
877+
878+
#[test]
879+
fn ambiguous_unary_operator_after_if_does_not_trigger_on_unit_type() {
880+
check(
881+
&wrap_in_callable("if true { 42; } else { 0; } - 1", CallableKind::Function),
882+
&expect![[r#"
883+
[]
884+
"#]],
885+
);
886+
}
887+
888+
#[test]
889+
fn ambiguous_unary_operator_after_if_does_not_trigger_for_unary_ops_besides_minus_plus() {
890+
check(
891+
&wrap_in_callable("if true { 42 } else { 0 } !1", CallableKind::Function),
892+
&expect![[r#"
893+
[]
894+
"#]],
895+
);
896+
}
897+
812898
#[test]
813899
fn check_that_hir_lints_are_deduplicated_in_operations_with_multiple_specializations() {
814900
check_with_deduplication(

source/vscode/qsharp.schema.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
"needlessOperation",
6666
"deprecatedFunctionConstructor",
6767
"deprecatedWithOperator",
68-
"deprecatedDoubleColonOperator"
68+
"deprecatedDoubleColonOperator",
69+
"ambiguousUnaryOperatorAfterIf"
6970
]
7071
},
7172
"level": {

0 commit comments

Comments
 (0)