From 1af2e7735283251c686bdb1154afab6df5e45053 Mon Sep 17 00:00:00 2001
From: sheaf <sam.derbyshire@gmail.com>
Date: Mon, 10 Jul 2023 16:38:10 +0200
Subject: [PATCH] Suggest similar names in imports
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This commit adds similar name suggestions when importing. For example

  module A where { spelling = 'o' }
  module B where { import B ( speling ) }

will give rise to the error message:

  Module ‘A’ does not export ‘speling’.
  Suggested fix: Perhaps use ‘spelling’

This also provides hints when users try to import record fields defined
with NoFieldSelectors.
---
 compiler/GHC/Rename/Names.hs                  | 64 ++++++++++++++-----
 compiler/GHC/Rename/Unbound.hs                | 10 +--
 compiler/GHC/Tc/Errors/Ppr.hs                 | 10 +--
 compiler/GHC/Tc/Errors/Types.hs               |  2 +-
 compiler/GHC/Types/Hint.hs                    |  2 +-
 compiler/GHC/Types/Hint/Ppr.hs                | 21 +++---
 .../should_compile/T22106_C.stderr            |  4 +-
 .../rename/should_fail/SimilarNamesImport.hs  |  3 +
 .../should_fail/SimilarNamesImport.stderr     | 16 +++++
 .../should_fail/SimilarNamesImport_aux.hs     | 11 ++++
 testsuite/tests/rename/should_fail/all.T      |  1 +
 11 files changed, 104 insertions(+), 40 deletions(-)
 create mode 100644 testsuite/tests/rename/should_fail/SimilarNamesImport.hs
 create mode 100644 testsuite/tests/rename/should_fail/SimilarNamesImport.stderr
 create mode 100644 testsuite/tests/rename/should_fail/SimilarNamesImport_aux.hs

diff --git a/compiler/GHC/Rename/Names.hs b/compiler/GHC/Rename/Names.hs
index af426dd1804a..be71785b5b00 100644
--- a/compiler/GHC/Rename/Names.hs
+++ b/compiler/GHC/Rename/Names.hs
@@ -38,6 +38,8 @@ import GHC.Driver.Ppr
 import GHC.Rename.Env
 import GHC.Rename.Fixity
 import GHC.Rename.Utils ( warnUnusedTopBinds )
+import GHC.Rename.Unbound
+import qualified GHC.Rename.Unbound as Unbound
 
 import GHC.Tc.Errors.Types
 import GHC.Tc.Utils.Env
@@ -67,6 +69,7 @@ import GHC.Types.Name.Set
 import GHC.Types.Name.Reader
 import GHC.Types.Avail
 import GHC.Types.FieldLabel
+import GHC.Types.Hint
 import GHC.Types.SourceFile
 import GHC.Types.SrcLoc as SrcLoc
 import GHC.Types.Basic  ( TopLevelFlag(..) )
@@ -1228,7 +1231,7 @@ filterImports hsc_env iface decl_spec (Just (want_hiding, L l import_items))
        = failLookupWith (QualImportError rdr)
        | otherwise
        = case lookups of
-           []         -> failLookupWith (BadImport ie BadImportIsParent)
+           []         -> failLookupWith (BadImport ie IsNotSubordinate)
            item:items -> return $ item :| items
       where
         lookups = concatMap nonDetNameEnvElts
@@ -1252,7 +1255,7 @@ filterImports hsc_env iface decl_spec (Just (want_hiding, L l import_items))
               -- 'BadImportW' is only constructed below in 'handle_bad_import', in
               -- the 'EverythingBut' case, so that's what we pass to
               -- 'badImportItemErr'.
-              reason <- badImportItemErr iface decl_spec ie BadImportIsParent all_avails
+              reason <- badImportItemErr iface decl_spec ie IsNotSubordinate all_avails
               pure (TcRnDodgyImports (DodgyImportsHiding reason))
             warning_msg (DeprecatedExport n w) =
               pure (TcRnPragmaWarning {
@@ -1338,7 +1341,7 @@ filterImports hsc_env iface decl_spec (Just (want_hiding, L l import_items))
                    dc_name = lookup_parent ie (setRdrNameSpace tc srcDataName)
                in
                case catIELookupM [ tc_name, dc_name ] of
-                 []    -> failLookupWith (BadImport ie BadImportIsParent)
+                 []    -> failLookupWith (BadImport ie IsNotSubordinate)
                  names -> return ( [mkIEThingAbs tc' l (imp_item name) | name <- names], [])
             | otherwise
             -> do ImpOccItem { imp_item = gre } <- lookup_parent ie (ieWrappedName tc')
@@ -1354,7 +1357,8 @@ filterImports hsc_env iface decl_spec (Just (want_hiding, L l import_items))
            -- See Note [Importing DuplicateRecordFields]
            case lookupChildren subnames rdr_ns of
 
-             Failed rdrs -> failLookupWith (BadImport (IEThingWith (deprecation, ann) ltc wc rdrs ) BadImportIsSubordinate)
+             Failed rdrs -> failLookupWith $
+                            BadImport (IEThingWith (deprecation, ann) ltc wc rdrs) IsSubordinate
                                 -- We are trying to import T( a,b,c,d ), and failed
                                 -- to find 'b' and 'd'.  So we make up an import item
                                 -- to report as failing, namely T( b, d ).
@@ -1382,7 +1386,9 @@ filterImports hsc_env iface decl_spec (Just (want_hiding, L l import_items))
           where n = greName gre
 
         handle_bad_import m = catchIELookup m $ \err -> case err of
-          BadImport ie _ | want_hiding == EverythingBut -> return ([], [BadImportW ie])
+          BadImport ie _
+            | want_hiding == EverythingBut
+            -> return ([], [BadImportW ie])
           _ -> failLookupWith err
 
         mk_depr_export_warning gre
@@ -1398,11 +1404,13 @@ data IELookupWarning
   | DodgyImport GlobalRdrElt
   | DeprecatedExport Name (WarningTxt GhcRn)
 
-data BadImportIsSubordinate = BadImportIsParent | BadImportIsSubordinate
+-- | Is this import/export item a subordinate or not?
+data IsSubordinate
+  = IsSubordinate | IsNotSubordinate
 
 data IELookupError
   = QualImportError RdrName
-  | BadImport (IE GhcPs) BadImportIsSubordinate
+  | BadImport (IE GhcPs) IsSubordinate
   | IllegalImport
 
 failLookupWith :: IELookupError -> IELookupM a
@@ -2151,21 +2159,42 @@ DRFPatSynExport for a test of this.
 -}
 
 badImportItemErr
-  :: ModIface -> ImpDeclSpec -> IE GhcPs -> BadImportIsSubordinate
+  :: ModIface -> ImpDeclSpec -> IE GhcPs -> IsSubordinate
   -> [AvailInfo]
   -> TcRn ImportLookupReason
 badImportItemErr iface decl_spec ie sub avails = do
   patsyns_enabled <- xoptM LangExt.PatternSynonyms
   expl_ns_enabled <- xoptM LangExt.ExplicitNamespaces
-  pure (ImportLookupBad (importErrorKind expl_ns_enabled) iface decl_spec ie patsyns_enabled)
+  dflags <- getDynFlags
+  hsc_env <- getTopEnv
+  let rdr_env = mkGlobalRdrEnv
+              $ gresFromAvails hsc_env (Just imp_spec) all_avails
+  pure (ImportLookupBad (importErrorKind dflags rdr_env expl_ns_enabled) iface decl_spec ie patsyns_enabled)
   where
-    importErrorKind expl_ns_enabled
+    importErrorKind dflags rdr_env expl_ns_enabled
       | any checkIfTyCon avails = case sub of
-          BadImportIsParent -> BadImportAvailTyCon expl_ns_enabled
-          BadImportIsSubordinate -> BadImportNotExportedSubordinates unavailableChildren
+          IsNotSubordinate -> BadImportAvailTyCon expl_ns_enabled
+          IsSubordinate -> BadImportNotExportedSubordinates unavailableChildren
       | any checkIfVarName avails = BadImportAvailVar
       | Just con <- find checkIfDataCon avails = BadImportAvailDataCon (availOccName con)
-      | otherwise = BadImportNotExported
+      | otherwise = BadImportNotExported suggs
+        where
+          suggs = similar_suggs ++ fieldSelectorSuggestions rdr_env rdr
+          similar_names =
+            similarNameSuggestions (Unbound.LF WL_Anything WL_Global)
+              dflags rdr_env emptyLocalRdrEnv rdr
+          similar_suggs =
+            case NE.nonEmpty $ mapMaybe imported_item $ similar_names of
+              Just similar -> [ SuggestSimilarNames rdr similar ]
+              Nothing      -> [ ]
+
+          -- Only keep imported items, and set the "HowInScope" to
+          -- "Nothing" to avoid printing "imported from..." in the suggestion
+          -- error message.
+          imported_item (SimilarRdrName rdr_name (Just (ImportedBy {})))
+            = Just (SimilarRdrName rdr_name Nothing)
+          imported_item _ = Nothing
+
     checkIfDataCon = checkIfAvailMatches isDataConName
     checkIfTyCon = checkIfAvailMatches isTyConName
     checkIfVarName =
@@ -2181,9 +2210,12 @@ badImportItemErr iface decl_spec ie sub avails = do
             Nothing -> False
         Avail{} -> False
     availOccName = occName . availName
-    importedFS = occNameFS . rdrNameOcc $ ieName ie
-    unavailableChildren = map (rdrNameOcc) $ case ie of
-      IEThingWith _ _ _ ns -> map (ieWrappedName  . unLoc) ns
+    rdr = ieName ie
+    importedFS = occNameFS $ rdrNameOcc rdr
+    imp_spec = ImpSpec { is_decl = decl_spec, is_item = ImpAll }
+    all_avails = mi_exports iface
+    unavailableChildren = case ie of
+      IEThingWith _ _ _ ns -> map (rdrNameOcc . ieWrappedName  . unLoc) ns
       _ -> panic "importedChildren failed pattern match: no children"
 
 addDupDeclErr :: NonEmpty GlobalRdrElt -> TcRn ()
diff --git a/compiler/GHC/Rename/Unbound.hs b/compiler/GHC/Rename/Unbound.hs
index 8c1b580ad150..744b42a0707b 100644
--- a/compiler/GHC/Rename/Unbound.hs
+++ b/compiler/GHC/Rename/Unbound.hs
@@ -15,6 +15,8 @@ module GHC.Rename.Unbound
    , reportUnboundName
    , reportUnboundName'
    , unknownNameSuggestions
+   , similarNameSuggestions
+   , fieldSelectorSuggestions
    , WhatLooking(..)
    , WhereLooking(..)
    , LookingFor(..)
@@ -225,7 +227,7 @@ similarNameSuggestions looking_for@(LF what_look where_look) dflags global_env
     all_possibilities :: [(String, SimilarName)]
     all_possibilities = case what_look of
       WL_None -> []
-      _ -> [ (showPpr dflags r, SimilarRdrName r (LocallyBoundAt loc))
+      _ -> [ (showPpr dflags r, SimilarRdrName r (Just $ LocallyBoundAt loc))
            | (r,loc) <- local_possibilities local_env ]
         ++ [ (showPpr dflags r, rp) | (r, rp) <- global_possibilities global_env ]
 
@@ -256,7 +258,7 @@ similarNameSuggestions looking_for@(LF what_look where_look) dflags global_env
 
     global_possibilities :: GlobalRdrEnv -> [(RdrName, SimilarName)]
     global_possibilities global_env
-      | tried_is_qual = [ (rdr_qual, SimilarRdrName rdr_qual how)
+      | tried_is_qual = [ (rdr_qual, SimilarRdrName rdr_qual (Just how))
                         | gre <- globalRdrEnvElts global_env
                         , isGreOk looking_for gre
                         , let occ = greOccName gre
@@ -271,7 +273,7 @@ similarNameSuggestions looking_for@(LF what_look where_look) dflags global_env
                           rdr_unqual = mkRdrUnqual occ
                     , correct_name_space occ
                     , sim <- case (unquals_in_scope gre, quals_only gre) of
-                                (how:_, _)    -> [ SimilarRdrName rdr_unqual how ]
+                                (how:_, _)    -> [ SimilarRdrName rdr_unqual (Just how) ]
                                 ([],    pr:_) -> [ pr ]  -- See Note [Only-quals]
                                 ([],    [])   -> [] ]
 
@@ -299,7 +301,7 @@ similarNameSuggestions looking_for@(LF what_look where_look) dflags global_env
     quals_only :: GlobalRdrElt -> [SimilarName]
     -- Ones for which *only* the qualified version is in scope
     quals_only (gre@GRE { gre_imp = is })
-      = [ (SimilarRdrName (mkRdrQual (is_as ispec) (greOccName gre)) (ImportedBy ispec))
+      = [ (SimilarRdrName (mkRdrQual (is_as ispec) (greOccName gre)) (Just $ ImportedBy ispec))
         | i <- bagToList is, let ispec = is_decl i, is_qual ispec ]
 
 
diff --git a/compiler/GHC/Tc/Errors/Ppr.hs b/compiler/GHC/Tc/Errors/Ppr.hs
index 002b1e7fc73e..65485293e513 100644
--- a/compiler/GHC/Tc/Errors/Ppr.hs
+++ b/compiler/GHC/Tc/Errors/Ppr.hs
@@ -3085,12 +3085,12 @@ instance Diagnostic TcRnMessage where
       let mod_name = moduleName $ is_mod is
           occ = rdrNameOcc $ ieName ie
       in case k of
-        BadImportAvailVar         -> [ImportSuggestion occ $ CouldRemoveTypeKeyword mod_name]
-        BadImportNotExported      -> noHints
-        BadImportAvailTyCon ex_ns ->
+        BadImportAvailVar          -> [ImportSuggestion occ $ CouldRemoveTypeKeyword mod_name]
+        BadImportNotExported suggs -> suggs
+        BadImportAvailTyCon ex_ns  ->
           [useExtensionInOrderTo empty LangExt.ExplicitNamespaces | not ex_ns]
           ++ [ImportSuggestion occ $ CouldAddTypeKeyword mod_name]
-        BadImportAvailDataCon par -> [ImportSuggestion occ $ ImportDataCon (Just (mod_name, patsyns_enabled)) par]
+        BadImportAvailDataCon par  -> [ImportSuggestion occ $ ImportDataCon (Just (mod_name, patsyns_enabled)) par]
         BadImportNotExportedSubordinates{} -> noHints
     TcRnImportLookup{}
       -> noHints
@@ -5343,7 +5343,7 @@ pprImportLookup = \case
         hang (text "In the import of" <+> pprImpDeclSpec iface decl_spec <> colon)
           2 (vcat msgs)
     in case k of
-      BadImportNotExported ->
+      BadImportNotExported _ ->
         vcat
           [ text "Module" <+> pprImpDeclSpec iface decl_spec <+>
             text "does not export" <+> quotes (ppr ie) <> dot
diff --git a/compiler/GHC/Tc/Errors/Types.hs b/compiler/GHC/Tc/Errors/Types.hs
index 43da4e8b8378..a65caa553dc8 100644
--- a/compiler/GHC/Tc/Errors/Types.hs
+++ b/compiler/GHC/Tc/Errors/Types.hs
@@ -5250,7 +5250,7 @@ data WhenMatching
 
 data BadImportKind
   -- | Module does not export...
-  = BadImportNotExported
+  = BadImportNotExported [GhcHint] -- ^ suggestions for what might have been meant
   -- | Missing @type@ keyword when importing a type.
   -- e.g.  `import TypeLits( (+) )`, where TypeLits exports a /type/ (+), not a /term/ (+)
   -- Then we want to suggest using `import TypeLits( type (+) )`
diff --git a/compiler/GHC/Types/Hint.hs b/compiler/GHC/Types/Hint.hs
index dc979918fdd3..0ef3968c9f08 100644
--- a/compiler/GHC/Types/Hint.hs
+++ b/compiler/GHC/Types/Hint.hs
@@ -515,7 +515,7 @@ data HowInScope
 
 data SimilarName
   = SimilarName Name
-  | SimilarRdrName RdrName HowInScope
+  | SimilarRdrName RdrName (Maybe HowInScope)
 
 -- | Something is promoted to the type-level without a promotion tick.
 data UntickedPromotedThing
diff --git a/compiler/GHC/Types/Hint/Ppr.hs b/compiler/GHC/Types/Hint/Ppr.hs
index b7d4f1e08fc8..c725c3cb3912 100644
--- a/compiler/GHC/Types/Hint/Ppr.hs
+++ b/compiler/GHC/Types/Hint/Ppr.hs
@@ -353,18 +353,17 @@ pprSimilarName :: NameSpace -> SimilarName -> SDoc
 pprSimilarName _ (SimilarName name)
   = quotes (ppr name) <+> parens (pprDefinedAt name)
 pprSimilarName tried_ns (SimilarRdrName rdr_name how_in_scope)
-  = case how_in_scope of
-      LocallyBoundAt loc ->
-        pp_ns rdr_name <+> quotes (ppr rdr_name) <+> loc'
-          where
-            loc' = case loc of
-              UnhelpfulSpan l -> parens (ppr l)
-              RealSrcSpan l _ -> parens (text "line" <+> int (srcSpanStartLine l))
-      ImportedBy is ->
-        pp_ns rdr_name <+> quotes (ppr rdr_name) <+>
-        parens (text "imported from" <+> ppr (moduleName $ is_mod is))
-
+  = pp_ns rdr_name <+> quotes (ppr rdr_name) <+> loc
   where
+    loc = case how_in_scope of
+      Nothing -> empty
+      Just scope -> case scope of
+        LocallyBoundAt loc ->
+          case loc of
+            UnhelpfulSpan l -> parens (ppr l)
+            RealSrcSpan l _ -> parens (text "line" <+> int (srcSpanStartLine l))
+        ImportedBy is ->
+          parens (text "imported from" <+> ppr (moduleName $ is_mod is))
     pp_ns :: RdrName -> SDoc
     pp_ns rdr | ns /= tried_ns = pprNameSpace ns
               | otherwise      = empty
diff --git a/testsuite/tests/overloadedrecflds/should_compile/T22106_C.stderr b/testsuite/tests/overloadedrecflds/should_compile/T22106_C.stderr
index f8d67446af16..774a3e3da075 100644
--- a/testsuite/tests/overloadedrecflds/should_compile/T22106_C.stderr
+++ b/testsuite/tests/overloadedrecflds/should_compile/T22106_C.stderr
@@ -1,6 +1,6 @@
 
-T22106_C.hs:5:9: error: [GHC-88464]
-    Variable not in scope: bar
+T22106_C.hs:3:21: error: [GHC-61689]
+    Module ‘T22106_aux’ does not export ‘bar’.
     Suggested fix:
       Notice that ‘bar’ is a field selector belonging to the type ‘T22106_aux.T’
       that has been suppressed by NoFieldSelectors.
diff --git a/testsuite/tests/rename/should_fail/SimilarNamesImport.hs b/testsuite/tests/rename/should_fail/SimilarNamesImport.hs
new file mode 100644
index 000000000000..8b15b72128ea
--- /dev/null
+++ b/testsuite/tests/rename/should_fail/SimilarNamesImport.hs
@@ -0,0 +1,3 @@
+module SimilarNamesImport where
+
+import SimilarNamesImport_aux ( dyzzy, Wabble, wabble, Trizzle(bizzy) )
diff --git a/testsuite/tests/rename/should_fail/SimilarNamesImport.stderr b/testsuite/tests/rename/should_fail/SimilarNamesImport.stderr
new file mode 100644
index 000000000000..528c1caad91c
--- /dev/null
+++ b/testsuite/tests/rename/should_fail/SimilarNamesImport.stderr
@@ -0,0 +1,16 @@
+
+SimilarNamesImport.hs:3:33: error: [GHC-61689]
+    Module ‘SimilarNamesImport_aux’ does not export ‘dyzzy’.
+    Suggested fix:
+      Perhaps use one of these: record field of MkD ‘dizzy’, ‘xyzzy’
+
+SimilarNamesImport.hs:3:40: error: [GHC-61689]
+    Module ‘SimilarNamesImport_aux’ does not export ‘Wabble’.
+    Suggested fix: Perhaps use ‘Wibble’
+
+SimilarNamesImport.hs:3:48: error: [GHC-61689]
+    Module ‘SimilarNamesImport_aux’ does not export ‘wabble’.
+
+SimilarNamesImport.hs:3:56: error: [GHC-61689]
+    Module ‘SimilarNamesImport_aux’ does not export ‘Trizzle’.
+    Suggested fix: Perhaps use one of these: ‘Drizzle’, ‘Frizzle’
diff --git a/testsuite/tests/rename/should_fail/SimilarNamesImport_aux.hs b/testsuite/tests/rename/should_fail/SimilarNamesImport_aux.hs
new file mode 100644
index 000000000000..254433036a5b
--- /dev/null
+++ b/testsuite/tests/rename/should_fail/SimilarNamesImport_aux.hs
@@ -0,0 +1,11 @@
+module SimilarNamesImport_aux where
+
+xyzzy :: Double
+xyzzy = exp $ pi * sqrt 163
+
+
+data Drizzle = MkD { dizzy :: Int }
+data Frizzle = MkE { fizzy :: Bool }
+
+data Wibble
+
diff --git a/testsuite/tests/rename/should_fail/all.T b/testsuite/tests/rename/should_fail/all.T
index 0ae4848a2ea1..749002bc4570 100644
--- a/testsuite/tests/rename/should_fail/all.T
+++ b/testsuite/tests/rename/should_fail/all.T
@@ -199,6 +199,7 @@ test('RnUnexpectedStandaloneDeriving', normal, compile_fail, [''])
 test('RnStupidThetaInGadt', normal, compile_fail, [''])
 test('PackageImportsDisabled', normal, compile_fail, [''])
 test('ImportLookupIllegal', normal, compile_fail, [''])
+test('SimilarNamesImport', [extra_files(['SimilarNamesImport_aux.hs'])], multimod_compile_fail, ['SimilarNamesImport', '-v0'])
 test('T23510a', normal, compile_fail, [''])
 test('T16635a', normal, compile_fail, [''])
 test('T16635b', normal, compile_fail, [''])
-- 
GitLab