11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveGeneric #-}
3+ {-# LANGUAGE DerivingStrategies #-}
34{-# LANGUAGE DuplicateRecordFields #-}
45{-# LANGUAGE FlexibleContexts #-}
56{-# LANGUAGE LambdaCase #-}
@@ -17,6 +18,7 @@ module Ide.Plugin.ExplicitFields
1718import Control.Lens ((^.) )
1819import Control.Monad.IO.Class (MonadIO , liftIO )
1920import Control.Monad.Trans.Except (ExceptT )
21+ import Data.Functor ((<&>) )
2022import Data.Generics (GenericQ , everything , extQ ,
2123 mkQ )
2224import qualified Data.HashMap.Strict as HashMap
@@ -38,11 +40,15 @@ import Development.IDE.GHC.Compat (HsConDetails (RecCon),
3840import Development.IDE.GHC.Compat.Core (Extension (NamedFieldPuns ),
3941 GhcPass ,
4042 HsExpr (RecordCon , rcon_flds ),
41- LHsExpr , Pass (.. ), Pat (.. ),
42- RealSrcSpan , conPatDetails ,
43- hfbPun , hs_valds ,
44- mapConPatDetail , mapLoc ,
45- pattern RealSrcSpan )
43+ HsRecField , LHsExpr , LocatedA ,
44+ Name , Pass (.. ), Pat (.. ),
45+ RealSrcSpan , UniqFM ,
46+ conPatDetails , emptyUFM ,
47+ hfbPun , hfbRHS , hs_valds ,
48+ lookupUFM , mapConPatDetail ,
49+ mapLoc , pattern RealSrcSpan ,
50+ plusUFM_C , ufmToIntMap ,
51+ unitUFM )
4652import Development.IDE.GHC.Util (getExtensions ,
4753 printOutputable )
4854import Development.IDE.Graph (RuleResult )
@@ -89,7 +95,7 @@ instance Pretty Log where
8995descriptor :: Recorder (WithPriority Log ) -> PluginId -> PluginDescriptor IdeState
9096descriptor recorder plId = (defaultPluginDescriptor plId)
9197 { pluginHandlers = mkPluginHandler STextDocumentCodeAction codeActionProvider
92- , pluginRules = collectRecordsRule recorder
98+ , pluginRules = collectRecordsRule recorder *> collectNamesRule
9399 }
94100
95101codeActionProvider :: PluginMethodHandler IdeState 'TextDocumentCodeAction
@@ -137,15 +143,21 @@ codeActionProvider ideState pId (CodeActionParams _ _ docId range _) = pluginRes
137143 title = " Expand record wildcard"
138144
139145collectRecordsRule :: Recorder (WithPriority Log ) -> Rules ()
140- collectRecordsRule recorder = define (cmapWithPrio LogShake recorder) $ \ CollectRecords nfp -> do
141- tmr <- use TypeCheck nfp
142- let exts = getEnabledExtensions <$> tmr
143- recs = concat $ maybeToList (getRecords <$> tmr)
144- logWith recorder Debug (LogCollectedRecords recs)
145- let renderedRecs = traverse renderRecordInfo recs
146- recMap = RangeMap. fromList (realSrcSpanToRange . renderedSrcSpan) <$> renderedRecs
147- logWith recorder Debug (LogRenderedRecords (concat renderedRecs))
148- pure ([] , CRR <$> recMap <*> exts)
146+ collectRecordsRule recorder = define (cmapWithPrio LogShake recorder) $ \ CollectRecords nfp ->
147+ use TypeCheck nfp >>= \ case
148+ Nothing -> pure ([] , Nothing )
149+ Just tmr -> do
150+ let exts = getEnabledExtensions tmr
151+ recs = getRecords tmr
152+ logWith recorder Debug (LogCollectedRecords recs)
153+ use CollectNames nfp >>= \ case
154+ Nothing -> pure ([] , Nothing )
155+ Just (CNR names) -> do
156+ let renderedRecs = traverse (renderRecordInfo names) recs
157+ recMap = RangeMap. fromList (realSrcSpanToRange . renderedSrcSpan) <$> renderedRecs
158+ logWith recorder Debug (LogRenderedRecords (concat renderedRecs))
159+ pure ([] , CRR <$> recMap <*> Just exts)
160+
149161 where
150162 getEnabledExtensions :: TcModuleResult -> [GhcExtension ]
151163 getEnabledExtensions = map GhcExtension . getExtensions . tmrParsed
@@ -154,6 +166,17 @@ getRecords :: TcModuleResult -> [RecordInfo]
154166getRecords (tmrRenamed -> (hs_valds -> valBinds,_,_,_)) =
155167 collectRecords valBinds
156168
169+ collectNamesRule :: Rules ()
170+ collectNamesRule = define mempty $ \ CollectNames nfp ->
171+ use TypeCheck nfp <&> \ case
172+ Nothing -> ([] , Nothing )
173+ Just tmr -> ([] , Just (CNR (getNames tmr)))
174+
175+ -- | Collects all 'Name's of a given source file, to be used
176+ -- in the variable usage analysis.
177+ getNames :: TcModuleResult -> NameMap
178+ getNames (tmrRenamed -> (group,_,_,_)) = NameMap (collectNames group)
179+
157180data CollectRecords = CollectRecords
158181 deriving (Eq , Show , Generic )
159182
@@ -173,13 +196,36 @@ instance Show CollectRecordsResult where
173196
174197type instance RuleResult CollectRecords = CollectRecordsResult
175198
199+ data CollectNames = CollectNames
200+ deriving (Eq , Show , Generic )
201+
202+ instance Hashable CollectNames
203+ instance NFData CollectNames
204+
205+ data CollectNamesResult = CNR NameMap
206+ deriving (Generic )
207+
208+ instance NFData CollectNamesResult
209+
210+ instance Show CollectNamesResult where
211+ show _ = " <CollectNamesResult>"
212+
213+ type instance RuleResult CollectNames = CollectNamesResult
214+
176215-- `Extension` is wrapped so that we can provide an `NFData` instance
177216-- (without resorting to creating an orphan instance).
178217newtype GhcExtension = GhcExtension { unExt :: Extension }
179218
180219instance NFData GhcExtension where
181220 rnf x = x `seq` ()
182221
222+ -- As with `GhcExtension`, this newtype exists mostly to attach
223+ -- an `NFData` instance to `UniqFM`.
224+ newtype NameMap = NameMap (UniqFM Name [Name ])
225+
226+ instance NFData NameMap where
227+ rnf (NameMap (ufmToIntMap -> m)) = rnf m
228+
183229data RecordInfo
184230 = RecordInfoPat RealSrcSpan (Pat (GhcPass 'Renamed))
185231 | RecordInfoCon RealSrcSpan (HsExpr (GhcPass 'Renamed))
@@ -199,10 +245,48 @@ instance Pretty RenderedRecordInfo where
199245
200246instance NFData RenderedRecordInfo
201247
202- renderRecordInfo :: RecordInfo -> Maybe RenderedRecordInfo
203- renderRecordInfo (RecordInfoPat ss pat) = RenderedRecordInfo ss <$> showRecordPat pat
204- renderRecordInfo (RecordInfoCon ss expr) = RenderedRecordInfo ss <$> showRecordCon expr
205-
248+ renderRecordInfo :: NameMap -> RecordInfo -> Maybe RenderedRecordInfo
249+ renderRecordInfo names (RecordInfoPat ss pat) = RenderedRecordInfo ss <$> showRecordPat names pat
250+ renderRecordInfo _ (RecordInfoCon ss expr) = RenderedRecordInfo ss <$> showRecordCon expr
251+
252+ -- | Checks if a 'Name' is referenced in the given map of names. The
253+ -- 'hasNonBindingOcc' check is necessary in order to make sure that only the
254+ -- references at the use-sites are considered (i.e. the binding occurence
255+ -- is excluded). For more information regarding the structure of the map,
256+ -- refer to the documentation of 'collectNames'.
257+ referencedIn :: Name -> NameMap -> Bool
258+ referencedIn name (NameMap names) = maybe True hasNonBindingOcc $ lookupUFM names name
259+ where
260+ hasNonBindingOcc :: [Name ] -> Bool
261+ hasNonBindingOcc = (> 1 ) . length
262+
263+ -- Default to leaving the element in if somehow a name can't be extracted (i.e.
264+ -- `getName` returns `Nothing`).
265+ filterReferenced :: (a -> Maybe Name ) -> NameMap -> [a ] -> [a ]
266+ filterReferenced getName names = filter (\ x -> maybe True (`referencedIn` names) (getName x))
267+
268+ preprocessRecordPat
269+ :: p ~ GhcPass 'Renamed
270+ => NameMap
271+ -> HsRecFields p (LPat p )
272+ -> HsRecFields p (LPat p )
273+ preprocessRecordPat = preprocessRecord (getFieldName . unLoc)
274+ where
275+ getFieldName x = case unLoc (hfbRHS x) of
276+ VarPat _ x' -> Just $ unLoc x'
277+ _ -> Nothing
278+
279+ -- No need to check the name usage in the record construction case
280+ preprocessRecordCon :: HsRecFields (GhcPass c ) arg -> HsRecFields (GhcPass c ) arg
281+ preprocessRecordCon = preprocessRecord (const Nothing ) (NameMap emptyUFM)
282+
283+ -- This function does two things:
284+ -- 1) Tweak the AST type so that the pretty-printed record is in the
285+ -- expanded form
286+ -- 2) Determine the unused record fields so that they are filtered out
287+ -- of the final output
288+ --
289+ -- Regarding first point:
206290-- We make use of the `Outputable` instances on AST types to pretty-print
207291-- the renamed and expanded records back into source form, to be substituted
208292-- with the original record later. However, `Outputable` instance of
@@ -212,8 +296,13 @@ renderRecordInfo (RecordInfoCon ss expr) = RenderedRecordInfo ss <$> showRecordC
212296-- as we want to print the records in their fully expanded form.
213297-- Here `rec_dotdot` is set to `Nothing` so that fields are printed without
214298-- such post-processing.
215- preprocessRecord :: HsRecFields (GhcPass c ) arg -> HsRecFields (GhcPass c ) arg
216- preprocessRecord flds = flds { rec_dotdot = Nothing , rec_flds = rec_flds' }
299+ preprocessRecord
300+ :: p ~ GhcPass c
301+ => (LocatedA (HsRecField p arg ) -> Maybe Name )
302+ -> NameMap
303+ -> HsRecFields p arg
304+ -> HsRecFields p arg
305+ preprocessRecord getName names flds = flds { rec_dotdot = Nothing , rec_flds = rec_flds' }
217306 where
218307 no_pun_count = maybe (length (rec_flds flds)) unLoc (rec_dotdot flds)
219308 -- Field binds of the explicit form (e.g. `{ a = a' }`) should be
@@ -223,29 +312,47 @@ preprocessRecord flds = flds { rec_dotdot = Nothing , rec_flds = rec_flds' }
223312 -- puns (since there is similar mechanism in the `Outputable` instance as
224313 -- explained above).
225314 puns' = map (mapLoc (\ fld -> fld { hfbPun = True })) puns
226- rec_flds' = no_puns <> puns'
227-
228- showRecordPat :: Outputable (Pat (GhcPass c )) => Pat (GhcPass c ) -> Maybe Text
229- showRecordPat = fmap printOutputable . mapConPatDetail (\ case
230- RecCon flds -> Just $ RecCon (preprocessRecord flds)
315+ -- Unused fields are filtered out so that they don't end up in the expanded
316+ -- form.
317+ punsUsed = filterReferenced getName names puns'
318+ rec_flds' = no_puns <> punsUsed
319+
320+ showRecordPat :: Outputable (Pat (GhcPass 'Renamed)) => NameMap -> Pat (GhcPass 'Renamed) -> Maybe Text
321+ showRecordPat names = fmap printOutputable . mapConPatDetail (\ case
322+ RecCon flds -> Just $ RecCon (preprocessRecordPat names flds)
231323 _ -> Nothing )
232324
233325showRecordCon :: Outputable (HsExpr (GhcPass c )) => HsExpr (GhcPass c ) -> Maybe Text
234326showRecordCon expr@ (RecordCon _ _ flds) =
235327 Just $ printOutputable $
236- expr { rcon_flds = preprocessRecord flds }
328+ expr { rcon_flds = preprocessRecordCon flds }
237329showRecordCon _ = Nothing
238330
239331collectRecords :: GenericQ [RecordInfo ]
240332collectRecords = everything (<>) (maybeToList . (Nothing `mkQ` getRecPatterns `extQ` getRecCons))
241333
334+ -- | Collect 'Name's into a map, indexed by the names' unique identifiers.
335+ -- The 'Eq' instance of 'Name's makes use of their unique identifiers, hence
336+ -- any 'Name' referring to the same entity is considered equal. In effect,
337+ -- each individual list of names contains the binding occurence, along with
338+ -- all the occurences at the use-sites (if there are any).
339+ --
340+ -- @UniqFM Name [Name]@ is morally the same as @Map Unique [Name]@.
341+ -- Using 'UniqFM' gains us a bit of performance (in theory) since it
342+ -- internally uses 'IntMap', and saves us rolling our own newtype wrapper over
343+ -- 'Unique' (since 'Unique' doesn't have an 'Ord' instance, it can't be used
344+ -- as 'Map' key as is). More information regarding 'UniqFM' can be found in
345+ -- the GHC source.
346+ collectNames :: GenericQ (UniqFM Name [Name ])
347+ collectNames = everything (plusUFM_C (<>) ) (emptyUFM `mkQ` (\ x -> unitUFM x [x]))
348+
242349getRecCons :: LHsExpr (GhcPass 'Renamed) -> Maybe RecordInfo
243350getRecCons e@ (unLoc -> RecordCon _ _ flds)
244351 | isJust (rec_dotdot flds) = mkRecInfo e
245352 where
246353 mkRecInfo :: LHsExpr (GhcPass 'Renamed) -> Maybe RecordInfo
247354 mkRecInfo expr = listToMaybe
248- [ RecordInfoCon realSpan (unLoc expr) | RealSrcSpan realSpan _ <- [ getLoc expr ]]
355+ [ RecordInfoCon realSpan' (unLoc expr) | RealSrcSpan realSpan' _ <- [ getLoc expr ]]
249356getRecCons _ = Nothing
250357
251358getRecPatterns :: LPat (GhcPass 'Renamed) -> Maybe RecordInfo
@@ -254,7 +361,7 @@ getRecPatterns conPat@(conPatDetails . unLoc -> Just (RecCon flds))
254361 where
255362 mkRecInfo :: LPat (GhcPass 'Renamed) -> Maybe RecordInfo
256363 mkRecInfo pat = listToMaybe
257- [ RecordInfoPat realSpan (unLoc pat) | RealSrcSpan realSpan _ <- [ getLoc pat ]]
364+ [ RecordInfoPat realSpan' (unLoc pat) | RealSrcSpan realSpan' _ <- [ getLoc pat ]]
258365getRecPatterns _ = Nothing
259366
260367collectRecords' :: MonadIO m => IdeState -> NormalizedFilePath -> ExceptT String m CollectRecordsResult
0 commit comments