about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMichael Goulet <michael@errs.io>2022-12-27 12:33:34 -0800
committerGitHub <noreply@github.com>2022-12-27 12:33:34 -0800
commita9fdeddafd8c5ce08dd8d61a86201ae9e1e71046 (patch)
tree98b05a2bb5bb36407c6697975aa5af18ecab47b7
parent4b668a1feed45b3061fbf0d1d42fb1a695c0d4b3 (diff)
parent7e84273b7fcaa0f6578c97fd71e27453ef300c31 (diff)
downloadrust-a9fdeddafd8c5ce08dd8d61a86201ae9e1e71046.tar.gz
rust-a9fdeddafd8c5ce08dd8d61a86201ae9e1e71046.zip
Rollup merge of #105765 - estebank:range-typo, r=compiler-errors
Detect likely `.` -> `..` typo in method calls

Fix #65015.
-rw-r--r--compiler/rustc_hir_typeck/src/demand.rs66
-rw-r--r--compiler/rustc_resolve/src/late.rs40
-rw-r--r--src/test/ui/suggestions/method-access-to-range-literal-typo.rs30
-rw-r--r--src/test/ui/suggestions/method-access-to-range-literal-typo.stderr48
4 files changed, 177 insertions, 7 deletions
diff --git a/compiler/rustc_hir_typeck/src/demand.rs b/compiler/rustc_hir_typeck/src/demand.rs
index 0335c45a946..1360383a75a 100644
--- a/compiler/rustc_hir_typeck/src/demand.rs
+++ b/compiler/rustc_hir_typeck/src/demand.rs
@@ -74,6 +74,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
         self.note_type_is_not_clone(err, expected, expr_ty, expr);
         self.note_need_for_fn_pointer(err, expected, expr_ty);
         self.note_internal_mutation_in_method(err, expr, expected, expr_ty);
+        self.check_for_range_as_method_call(err, expr, expr_ty, expected);
     }
 
     /// Requires that the two types unify, and prints an error message if
@@ -1607,4 +1608,69 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
             _ => false,
         }
     }
+
+    /// Identify when the user has written `foo..bar()` instead of `foo.bar()`.
+    pub fn check_for_range_as_method_call(
+        &self,
+        err: &mut Diagnostic,
+        expr: &hir::Expr<'_>,
+        checked_ty: Ty<'tcx>,
+        expected_ty: Ty<'tcx>,
+    ) {
+        if !hir::is_range_literal(expr) {
+            return;
+        }
+        let hir::ExprKind::Struct(
+            hir::QPath::LangItem(LangItem::Range, ..),
+            [start, end],
+            _,
+        ) = expr.kind else { return; };
+        let parent = self.tcx.hir().get_parent_node(expr.hir_id);
+        if let Some(hir::Node::ExprField(_)) = self.tcx.hir().find(parent) {
+            // Ignore `Foo { field: a..Default::default() }`
+            return;
+        }
+        let mut expr = end.expr;
+        while let hir::ExprKind::MethodCall(_, rcvr, ..) = expr.kind {
+            // Getting to the root receiver and asserting it is a fn call let's us ignore cases in
+            // `src/test/ui/methods/issues/issue-90315.stderr`.
+            expr = rcvr;
+        }
+        let hir::ExprKind::Call(method_name, _) = expr.kind else { return; };
+        let ty::Adt(adt, _) = checked_ty.kind() else { return; };
+        if self.tcx.lang_items().range_struct() != Some(adt.did()) {
+            return;
+        }
+        if let ty::Adt(adt, _) = expected_ty.kind()
+            && self.tcx.lang_items().range_struct() == Some(adt.did())
+        {
+            return;
+        }
+        // Check if start has method named end.
+        let hir::ExprKind::Path(hir::QPath::Resolved(None, p)) = method_name.kind else { return; };
+        let [hir::PathSegment { ident, .. }] = p.segments else { return; };
+        let self_ty = self.typeck_results.borrow().expr_ty(start.expr);
+        let Ok(_pick) = self.probe_for_name(
+            probe::Mode::MethodCall,
+            *ident,
+            probe::IsSuggestion(true),
+            self_ty,
+            expr.hir_id,
+            probe::ProbeScope::AllTraits,
+        ) else { return; };
+        let mut sugg = ".";
+        let mut span = start.expr.span.between(end.expr.span);
+        if span.lo() + BytePos(2) == span.hi() {
+            // There's no space between the start, the range op and the end, suggest removal which
+            // will be more noticeable than the replacement of `..` with `.`.
+            span = span.with_lo(span.lo() + BytePos(1));
+            sugg = "";
+        }
+        err.span_suggestion_verbose(
+            span,
+            "you likely meant to write a method call instead of a range",
+            sugg,
+            Applicability::MachineApplicable,
+        );
+    }
 }
diff --git a/compiler/rustc_resolve/src/late.rs b/compiler/rustc_resolve/src/late.rs
index 5b7a00101e9..49e069f58c9 100644
--- a/compiler/rustc_resolve/src/late.rs
+++ b/compiler/rustc_resolve/src/late.rs
@@ -16,7 +16,7 @@ use rustc_ast::ptr::P;
 use rustc_ast::visit::{self, AssocCtxt, BoundKind, FnCtxt, FnKind, Visitor};
 use rustc_ast::*;
 use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap};
-use rustc_errors::{DiagnosticArgValue, DiagnosticId, IntoDiagnosticArg};
+use rustc_errors::{Applicability, DiagnosticArgValue, DiagnosticId, IntoDiagnosticArg};
 use rustc_hir::def::Namespace::{self, *};
 use rustc_hir::def::{self, CtorKind, DefKind, LifetimeRes, PartialRes, PerNS};
 use rustc_hir::def_id::{DefId, LocalDefId, CRATE_DEF_ID, LOCAL_CRATE};
@@ -536,6 +536,9 @@ struct DiagnosticMetadata<'ast> {
     in_assignment: Option<&'ast Expr>,
     is_assign_rhs: bool,
 
+    /// Used to detect possible `.` -> `..` typo when calling methods.
+    in_range: Option<(&'ast Expr, &'ast Expr)>,
+
     /// If we are currently in a trait object definition. Used to point at the bounds when
     /// encountering a struct or enum.
     current_trait_object: Option<&'ast [ast::GenericBound]>,
@@ -3320,6 +3323,7 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
         );
     }
 
+    #[instrument(level = "debug", skip(self))]
     fn smart_resolve_path_fragment(
         &mut self,
         qself: &Option<P<QSelf>>,
@@ -3327,10 +3331,6 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
         source: PathSource<'ast>,
         finalize: Finalize,
     ) -> PartialRes {
-        debug!(
-            "smart_resolve_path_fragment(qself={:?}, path={:?}, finalize={:?})",
-            qself, path, finalize,
-        );
         let ns = source.namespace();
 
         let Finalize { node_id, path_span, .. } = finalize;
@@ -3341,8 +3341,28 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
 
                 let def_id = this.parent_scope.module.nearest_parent_mod();
                 let instead = res.is_some();
-                let suggestion =
-                    if res.is_none() { this.report_missing_type_error(path) } else { None };
+                let suggestion = if let Some((start, end)) = this.diagnostic_metadata.in_range
+                    && path[0].ident.span.lo() == end.span.lo()
+                {
+                    let mut sugg = ".";
+                    let mut span = start.span.between(end.span);
+                    if span.lo() + BytePos(2) == span.hi() {
+                        // There's no space between the start, the range op and the end, suggest
+                        // removal which will look better.
+                        span = span.with_lo(span.lo() + BytePos(1));
+                        sugg = "";
+                    }
+                    Some((
+                        span,
+                        "you might have meant to write `.` instead of `..`",
+                        sugg.to_string(),
+                        Applicability::MaybeIncorrect,
+                    ))
+                } else if res.is_none() {
+                    this.report_missing_type_error(path)
+                } else {
+                    None
+                };
 
                 this.r.use_injections.push(UseError {
                     err,
@@ -4005,6 +4025,12 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
                 self.visit_expr(rhs);
                 self.diagnostic_metadata.is_assign_rhs = false;
             }
+            ExprKind::Range(Some(ref start), Some(ref end), RangeLimits::HalfOpen) => {
+                self.diagnostic_metadata.in_range = Some((start, end));
+                self.resolve_expr(start, Some(expr));
+                self.resolve_expr(end, Some(expr));
+                self.diagnostic_metadata.in_range = None;
+            }
             _ => {
                 visit::walk_expr(self, expr);
             }
diff --git a/src/test/ui/suggestions/method-access-to-range-literal-typo.rs b/src/test/ui/suggestions/method-access-to-range-literal-typo.rs
new file mode 100644
index 00000000000..ac662edafe6
--- /dev/null
+++ b/src/test/ui/suggestions/method-access-to-range-literal-typo.rs
@@ -0,0 +1,30 @@
+fn as_ref() -> Option<Vec<u8>> {
+    None
+}
+struct Type {
+    option: Option<Vec<u8>>
+}
+trait Trait {
+    fn foo(&self) -> Vec<u8>;
+}
+impl Trait for Option<Vec<u8>> {
+    fn foo(&self) -> Vec<u8> {
+        vec![1, 2, 3]
+    }
+}
+
+impl Type {
+    fn method(&self) -> Option<Vec<u8>> {
+        self.option..as_ref().map(|x| x)
+        //~^ ERROR E0308
+    }
+    fn method2(&self) -> &u8 {
+        self.option..foo().get(0)
+        //~^ ERROR E0425
+        //~| ERROR E0308
+    }
+}
+
+fn main() {
+    let _ = Type { option: None }.method();
+}
diff --git a/src/test/ui/suggestions/method-access-to-range-literal-typo.stderr b/src/test/ui/suggestions/method-access-to-range-literal-typo.stderr
new file mode 100644
index 00000000000..c84f9467891
--- /dev/null
+++ b/src/test/ui/suggestions/method-access-to-range-literal-typo.stderr
@@ -0,0 +1,48 @@
+error[E0425]: cannot find function `foo` in this scope
+  --> $DIR/method-access-to-range-literal-typo.rs:22:22
+   |
+LL |         self.option..foo().get(0)
+   |                      ^^^ not found in this scope
+   |
+help: you might have meant to write `.` instead of `..`
+   |
+LL -         self.option..foo().get(0)
+LL +         self.option.foo().get(0)
+   |
+
+error[E0308]: mismatched types
+  --> $DIR/method-access-to-range-literal-typo.rs:18:9
+   |
+LL |     fn method(&self) -> Option<Vec<u8>> {
+   |                         --------------- expected `Option<Vec<u8>>` because of return type
+LL |         self.option..as_ref().map(|x| x)
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected enum `Option`, found struct `Range`
+   |
+   = note: expected enum `Option<_>`
+            found struct `std::ops::Range<Option<_>>`
+help: you likely meant to write a method call instead of a range
+   |
+LL -         self.option..as_ref().map(|x| x)
+LL +         self.option.as_ref().map(|x| x)
+   |
+
+error[E0308]: mismatched types
+  --> $DIR/method-access-to-range-literal-typo.rs:22:9
+   |
+LL |     fn method2(&self) -> &u8 {
+   |                          --- expected `&u8` because of return type
+LL |         self.option..foo().get(0)
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&u8`, found struct `Range`
+   |
+   = note: expected reference `&u8`
+                 found struct `std::ops::Range<Option<Vec<u8>>>`
+help: you likely meant to write a method call instead of a range
+   |
+LL -         self.option..foo().get(0)
+LL +         self.option.foo().get(0)
+   |
+
+error: aborting due to 3 previous errors
+
+Some errors have detailed explanations: E0308, E0425.
+For more information about an error, try `rustc --explain E0308`.