diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e921a9b85e..8a86340c4e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -236,6 +236,10 @@ jobs: name: Test hls-hlint-plugin test suite run: cabal test hls-hlint-plugin --test-options="$TEST_OPTS" || cabal test hls-hlint-plugin --test-options="$TEST_OPTS" || LSP_TEST_LOG_COLOR=0 LSP_TEST_LOG_MESSAGES=true LSP_TEST_LOG_STDERR=true cabal test hls-hlint-plugin --test-options="$TEST_OPTS" + - if: matrix.test + name: Test hls-qualify-imported-names-plugin test suite + run: cabal test hls-qualify-imported-names-plugin --test-options="-j1 --rerun-update" || cabal test hls-qualify-imported-names-plugin --test-options="-j1 --rerun" || LSP_TEST_LOG_COLOR=0 LSP_TEST_LOG_MESSAGES=true LSP_TEST_LOG_STDERR=true cabal test hls-qualify-imported-names-plugin --test-options="-j1 --rerun" + test_post_job: if: always() runs-on: ubuntu-latest diff --git a/cabal-ghc901.project b/cabal-ghc901.project index 3010a6e0c0..4fc039e768 100644 --- a/cabal-ghc901.project +++ b/cabal-ghc901.project @@ -19,6 +19,7 @@ packages: ./plugins/hls-retrie-plugin ./plugins/hls-haddock-comments-plugin ./plugins/hls-splice-plugin + ./plugins/hls-qualify-imported-names-plugin ./plugins/hls-floskell-plugin ./plugins/hls-pragmas-plugin ./plugins/hls-module-name-plugin diff --git a/cabal-ghc921.project b/cabal-ghc921.project index 60cccb7373..9b1795d445 100644 --- a/cabal-ghc921.project +++ b/cabal-ghc921.project @@ -18,6 +18,7 @@ packages: -- ./plugins/hls-retrie-plugin ./plugins/hls-haddock-comments-plugin -- ./plugins/hls-splice-plugin + ./plugins/hls-qualify-imported-names-plugin ./plugins/hls-floskell-plugin ./plugins/hls-pragmas-plugin ./plugins/hls-module-name-plugin diff --git a/cabal.project b/cabal.project index c5280e6b33..c44a919bbf 100644 --- a/cabal.project +++ b/cabal.project @@ -24,6 +24,8 @@ packages: ./plugins/hls-module-name-plugin ./plugins/hls-ormolu-plugin ./plugins/hls-call-hierarchy-plugin + ./plugins/hls-qualify-imported-names-plugin + -- Standard location for temporary packages needed for particular environments -- For example it is used in the project gitlab mirror to help in the MAcOS M1 build script diff --git a/docs/features.md b/docs/features.md index 84550d0a17..1db259894b 100644 --- a/docs/features.md +++ b/docs/features.md @@ -16,6 +16,7 @@ You can watch demos for some of these features [below](#demos). - [Integration](#hlint) with [hlint](https://github.com/ndmitchell/hlint), the most used haskell linter, to show diagnostics and apply hints via [apply-refact](https://github.com/mpickering/apply-refact) - [Module name suggestions](#module-names) for insertion or correction - [Call hierarchy support](#call-hierarchy) +- [Qualify names from an import declaration](#qualify-imported-names) in your code ## Demos @@ -46,3 +47,7 @@ You can watch demos for some of these features [below](#demos). ### Call hierarchy ![Call Hierarchy in VSCode](https://github.com/haskell/haskell-language-server/raw/2857eeece0398e1cd4b2ffb6069b05c4d2308b39/plugins/hls-call-hierarchy-plugin/call-hierarchy-in-vscode.gif) + +### Qualify imported names + +![Qualify Imported Names Demo](../plugins/hls-qualify-imported-names-plugin/qualify-imported-names-demo.gif) diff --git a/exe/Plugins.hs b/exe/Plugins.hs index 5e4a6165d9..c8f6a812db 100644 --- a/exe/Plugins.hs +++ b/exe/Plugins.hs @@ -12,6 +12,9 @@ import Ide.Plugin.Example as Example import Ide.Plugin.Example2 as Example2 -- haskell-language-server optional plugins +#if qualifyImportedNames +import Ide.Plugin.QualifyImportedNames as QualifyImportedNames +#endif #if callHierarchy import Ide.Plugin.CallHierarchy as CallHierarchy @@ -143,6 +146,9 @@ idePlugins includeExamples = pluginDescToIdePlugins allPlugins #if importLens ExplicitImports.descriptor "importLens" : #endif +#if qualifyImportedNames + QualifyImportedNames.descriptor "qualifyImportedNames" : +#endif #if refineImports RefineImports.descriptor "refineImports" : #endif diff --git a/ghcide/src/Development/IDE/GHC/Compat/Core.hs b/ghcide/src/Development/IDE/GHC/Compat/Core.hs index 6bc9e50f32..634b530c8b 100644 --- a/ghcide/src/Development/IDE/GHC/Compat/Core.hs +++ b/ghcide/src/Development/IDE/GHC/Compat/Core.hs @@ -287,6 +287,7 @@ module Development.IDE.GHC.Compat.Core ( module GHC.Core.DataCon, module GHC.Core.FamInstEnv, module GHC.Core.InstEnv, + module GHC.Types.Unique.FM, #if !MIN_VERSION_ghc(9,2,0) module GHC.Core.Ppr.TyThing, #endif @@ -380,6 +381,7 @@ module Development.IDE.GHC.Compat.Core ( module TysWiredIn, module Type, module Unify, + module UniqFM, module UniqSupply, module Var, #endif @@ -426,6 +428,7 @@ import GHC.Core.DataCon hiding (dataConExTyCoVars) import qualified GHC.Core.DataCon as DataCon import GHC.Core.FamInstEnv import GHC.Core.InstEnv +import GHC.Types.Unique.FM #if MIN_VERSION_ghc(9,2,0) import GHC.Core.Multiplicity (scaledThing) #else @@ -512,7 +515,9 @@ import GHC.Types.TyThing.Ppr #else import GHC.Types.Name.Set #endif -import GHC.Types.SrcLoc (BufPos, BufSpan, SrcSpan (UnhelpfulSpan), SrcLoc(UnhelpfulLoc)) +import GHC.Types.SrcLoc (BufPos, BufSpan, + SrcLoc (UnhelpfulLoc), + SrcSpan (UnhelpfulSpan)) import qualified GHC.Types.SrcLoc as SrcLoc import GHC.Types.Unique.Supply import GHC.Types.Var (Var (varName), setTyVarUnique, @@ -631,6 +636,7 @@ import Type hiding (mkVisFunTys) import TysPrim import TysWiredIn import Unify +import UniqFM import UniqSupply import Var (Var (varName), setTyVarUnique, setVarUnique, varType) @@ -638,14 +644,15 @@ import Var (Var (varName), setTyVarUnique, #if MIN_VERSION_ghc(8,10,0) import Coercion (coercionKind) import Predicate -import SrcLoc (SrcSpan (UnhelpfulSpan), SrcLoc (UnhelpfulLoc)) +import SrcLoc (SrcLoc (UnhelpfulLoc), + SrcSpan (UnhelpfulSpan)) #else -import SrcLoc (RealLocated, - SrcSpan (UnhelpfulSpan), - SrcLoc (UnhelpfulLoc)) +import SrcLoc (RealLocated, SrcLoc (UnhelpfulLoc), + SrcSpan (UnhelpfulSpan)) #endif #endif + #if !MIN_VERSION_ghc(8,8,0) import Data.List (isSuffixOf) import System.FilePath diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index f2aff3e1b0..2fda1fb4fe 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -156,6 +156,11 @@ flag splice default: True manual: True +flag qualifyImportedNames + description: Enable qualifyImportedNames plugin + default: True + manual: True + -- formatters flag floskell @@ -253,6 +258,11 @@ common splice build-depends: hls-splice-plugin ^>=1.0.0.1 cpp-options: -Dsplice +common qualifyImportedNames + if flag(qualifyImportedNames) + build-depends: hls-qualify-imported-names-plugin ^>=1.0.0.0 + cpp-options: -DqualifyImportedNames + -- formatters common floskell @@ -297,6 +307,7 @@ executable haskell-language-server , moduleName , pragmas , splice + , qualifyImportedNames , floskell , fourmolu , ormolu diff --git a/plugins/hls-qualify-imported-names-plugin/LICENSE b/plugins/hls-qualify-imported-names-plugin/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/hls-qualify-imported-names-plugin/README.md b/plugins/hls-qualify-imported-names-plugin/README.md new file mode 100644 index 0000000000..f7a20fa216 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/README.md @@ -0,0 +1,19 @@ +# Qualify Imported Names + +![Qualify Imported Names Demo](qualify-imported-names-demo.gif) + +## Usage + +1. Put cursor over the import declaration you want to qualify names from. +2. Initiate a Code Action. +3. Select `Qualify imported names`. + +## Features +- Names are qualified on a per-import declaration basis. +- Names are qualified by the imported module's alias if it has one, otherwise by the imported module's name. +- If the import declaration has an explicit import list then the plugin will qualify only names on the list. +- If the import declaration has an explicit hiding list then the plugin will qualify names from the imported module that are not on the list. + +## Change log +### 1.0.0.0 +- Released... diff --git a/plugins/hls-qualify-imported-names-plugin/hls-qualify-imported-names-plugin.cabal b/plugins/hls-qualify-imported-names-plugin/hls-qualify-imported-names-plugin.cabal new file mode 100644 index 0000000000..3d5e9a7a5b --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/hls-qualify-imported-names-plugin.cabal @@ -0,0 +1,54 @@ +cabal-version: 2.2 +name: hls-qualify-imported-names-plugin +version: 1.0.0.0 +synopsis: A Haskell Language Server plugin that qualifies imported names +description: + Please see the README on GitHub at +license: Apache-2.0 +license-file: LICENSE +author: Jonathan Shen +maintainer: shenjonathan0@gmail.com +category: Development +build-type: Simple +extra-source-files: + LICENSE + qualify-imported-names-demo.gif + README.md + test/data/*.hs + test/data/*.yaml + +library + exposed-modules: Ide.Plugin.QualifyImportedNames + hs-source-dirs: src + build-depends: + , aeson + , base >=4.12 && <5 + , containers + , deepseq + , ghc + , ghcide ^>=1.5.0 + , hls-graph + , hls-plugin-api >=1.1 && <1.3 + , lsp + , text + , unordered-containers + , dlist + , transformers + + default-language: Haskell2010 + default-extensions: + DataKinds + TypeOperators + +test-suite tests + type: exitcode-stdio-1.0 + default-language: Haskell2010 + hs-source-dirs: test + main-is: Main.hs + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: + , base + , text + , filepath + , hls-qualify-imported-names-plugin + , hls-test-utils >=1.0 && <1.2 diff --git a/plugins/hls-qualify-imported-names-plugin/qualify-imported-names-demo.gif b/plugins/hls-qualify-imported-names-plugin/qualify-imported-names-demo.gif new file mode 100644 index 0000000000..197a1eee49 Binary files /dev/null and b/plugins/hls-qualify-imported-names-plugin/qualify-imported-names-demo.gif differ diff --git a/plugins/hls-qualify-imported-names-plugin/src/Ide/Plugin/QualifyImportedNames.hs b/plugins/hls-qualify-imported-names-plugin/src/Ide/Plugin/QualifyImportedNames.hs new file mode 100644 index 0000000000..2aa31fcf5b --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/src/Ide/Plugin/QualifyImportedNames.hs @@ -0,0 +1,259 @@ +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ViewPatterns #-} + +module Ide.Plugin.QualifyImportedNames (descriptor) where + +import Control.Monad (foldM) +import Control.Monad.IO.Class (MonadIO (liftIO)) +import Control.Monad.Trans.State.Strict (State) +import qualified Control.Monad.Trans.State.Strict as State +import Data.DList (DList) +import qualified Data.DList as DList +import Data.Foldable (Foldable (foldl'), find) +import qualified Data.HashMap.Strict as HashMap +import Data.List (sortOn) +import qualified Data.List as List +import qualified Data.Map.Strict as Map +import Data.Maybe (mapMaybe) +import Data.Text (Text) +import qualified Data.Text as Text +import Development.IDE.Core.RuleTypes (GetFileContents (GetFileContents), + GetHieAst (GetHieAst), + HieAstResult (HAR, refMap), + TcModuleResult (TcModuleResult, tmrParsed, tmrTypechecked), + TypeCheck (TypeCheck)) +import Development.IDE.Core.Service (runAction) +import Development.IDE.Core.Shake (IdeState, use) +import Development.IDE.GHC.Compat (ContextInfo (Use), + Identifier, + IdentifierDetails (IdentifierDetails, identInfo), + RefMap, Span) +import Development.IDE.GHC.Compat.Core (GenLocated (L), GhcPs, + GlobalRdrElt (GRE, gre_imp, gre_name), + GlobalRdrEnv, + HsModule (hsmodImports), + ImpDeclSpec (ImpDeclSpec, is_as, is_dloc, is_qual), + ImportSpec (ImpSpec), + LImportDecl, ModuleName, + Name, NameEnv, OccName, + ParsedModule (ParsedModule, pm_parsed_source), + SrcSpan, + TcGblEnv (tcg_rdr_env), + emptyUFM, globalRdrEnvElts, + lookupNameEnv, + moduleNameString, + nameOccName, occNameString, + plusUFM_C, srcSpanEndCol, + srcSpanEndLine, + srcSpanStartCol, + srcSpanStartLine, unitUFM) +import Development.IDE.GHC.Error (isInsideSrcSpan) +import Development.IDE.Types.Diagnostics (List (List)) +import Development.IDE.Types.Location (NormalizedFilePath, + Position (Position), + Range (Range), Uri, + toNormalizedUri) +import Ide.Types (PluginDescriptor (pluginHandlers), + PluginId, + PluginMethodHandler, + defaultPluginDescriptor, + mkPluginHandler) +import Language.LSP.Types (CodeAction (CodeAction, _command, _diagnostics, _disabled, _edit, _isPreferred, _kind, _title, _xdata), + CodeActionKind (CodeActionQuickFix), + CodeActionParams (CodeActionParams), + Method (TextDocumentCodeAction), + SMethod (STextDocumentCodeAction), + TextDocumentIdentifier (TextDocumentIdentifier), + TextEdit (TextEdit), + WorkspaceEdit (WorkspaceEdit, _changeAnnotations, _changes, _documentChanges), + type (|?) (InR), + uriToNormalizedFilePath) + +thenCmp :: Ordering -> Ordering -> Ordering +{-# INLINE thenCmp #-} +thenCmp EQ ordering = ordering +thenCmp ordering _ = ordering + +descriptor :: PluginId -> PluginDescriptor IdeState +descriptor pluginId = (defaultPluginDescriptor pluginId) { + pluginHandlers = mconcat + [ mkPluginHandler STextDocumentCodeAction codeActionProvider + ] +} + +isRangeWithinSrcSpan :: Range -> SrcSpan -> Bool +isRangeWithinSrcSpan (Range start end) srcSpan = + isInsideSrcSpan start srcSpan && isInsideSrcSpan end srcSpan + +findLImportDeclAt :: Range -> ParsedModule -> Maybe (LImportDecl GhcPs) +findLImportDeclAt range parsedModule + | ParsedModule {..} <- parsedModule + , L _ hsModule <- pm_parsed_source + , locatedImportDecls <- hsmodImports hsModule = + find (\ (L srcSpan _) -> isRangeWithinSrcSpan range srcSpan) locatedImportDecls + | otherwise = Nothing + +makeCodeActions :: Uri -> [TextEdit] -> [a |? CodeAction] +makeCodeActions uri textEdits = [InR CodeAction {..} | not (null textEdits)] + where _title = "Qualify imported names" + _kind = Just CodeActionQuickFix + _command = Nothing + _edit = Just WorkspaceEdit {..} + _changes = Just $ HashMap.singleton uri $ List textEdits + _documentChanges = Nothing + _diagnostics = Nothing + _isPreferred = Nothing + _disabled = Nothing + _xdata = Nothing + _changeAnnotations = Nothing + +getTypeCheckedModule :: IdeState -> NormalizedFilePath -> IO (Maybe TcModuleResult) +getTypeCheckedModule ideState normalizedFilePath = + runAction "QualifyImportedNames.TypeCheck" ideState (use TypeCheck normalizedFilePath) + +getHieAst :: IdeState -> NormalizedFilePath -> IO (Maybe HieAstResult) +getHieAst ideState normalizedFilePath = + runAction "QualifyImportedNames.GetHieAst" ideState (use GetHieAst normalizedFilePath) + +getSourceText :: IdeState -> NormalizedFilePath -> IO (Maybe Text) +getSourceText ideState normalizedFilePath = do + fileContents <- runAction "QualifyImportedNames.GetFileContents" ideState (use GetFileContents normalizedFilePath) + if | Just (_, sourceText) <- fileContents -> pure sourceText + | otherwise -> pure Nothing + +data ImportedBy = ImportedBy { + importedByAlias :: !ModuleName, + importedBySrcSpan :: !SrcSpan +} + +isRangeWithinImportedBy :: Range -> ImportedBy -> Bool +isRangeWithinImportedBy range (ImportedBy _ srcSpan) = isRangeWithinSrcSpan range srcSpan + +globalRdrEnvToNameToImportedByMap :: GlobalRdrEnv -> NameEnv [ImportedBy] +globalRdrEnvToNameToImportedByMap = + fmap DList.toList . foldl' (plusUFM_C (<>)) emptyUFM . map globalRdrEltToNameToImportedByMap . globalRdrEnvElts + where + globalRdrEltToNameToImportedByMap :: GlobalRdrElt -> NameEnv (DList ImportedBy) + globalRdrEltToNameToImportedByMap GRE {..} = + unitUFM gre_name $ DList.fromList $ mapMaybe importSpecToImportedBy gre_imp + + importSpecToImportedBy :: ImportSpec -> Maybe ImportedBy + importSpecToImportedBy (ImpSpec ImpDeclSpec {..} _) + | is_qual = Nothing + | otherwise = Just (ImportedBy is_as is_dloc) + +data IdentifierSpan = IdentifierSpan { + identifierSpanLine :: !Int, + identifierSpanStartCol :: !Int, + identifierSpanEndCol :: !Int +} deriving (Show, Eq) + +instance Ord IdentifierSpan where + compare (IdentifierSpan line1 startCol1 endCol1) (IdentifierSpan line2 startCol2 endCol2) = + (line1 `compare` line2) `thenCmp` (startCol1 `compare` startCol2) `thenCmp` (endCol1 `compare` endCol2) + +realSrcSpanToIdentifierSpan :: Span -> Maybe IdentifierSpan +realSrcSpanToIdentifierSpan realSrcSpan + | let startLine = srcSpanStartLine realSrcSpan - 1 + , let endLine = srcSpanEndLine realSrcSpan - 1 + , startLine == endLine + , let startCol = srcSpanStartCol realSrcSpan - 1 + , let endCol = srcSpanEndCol realSrcSpan - 1 = + Just $ IdentifierSpan startLine startCol endCol + | otherwise = Nothing + +identifierSpanToRange :: IdentifierSpan -> Range +identifierSpanToRange (IdentifierSpan line startCol endCol) = + Range (Position line startCol) (Position line endCol) + +data UsedIdentifier = UsedIdentifier { + usedIdentifierName :: !Name, + usedIdentifierSpan :: !IdentifierSpan +} + +refMapToUsedIdentifiers :: RefMap a -> [UsedIdentifier] +refMapToUsedIdentifiers = DList.toList . Map.foldlWithKey' folder DList.empty + where + folder acc identifier spanIdentifierDetailsPairs = + DList.fromList (mapMaybe (uncurry (getUsedIdentifier identifier)) spanIdentifierDetailsPairs) <> acc + + getUsedIdentifier :: Identifier -> Span -> IdentifierDetails a -> Maybe UsedIdentifier + getUsedIdentifier identifier span IdentifierDetails {..} + | Just identifierSpan <- realSrcSpanToIdentifierSpan span + , Right name <- identifier + , Use `elem` identInfo = Just $ UsedIdentifier name identifierSpan + | otherwise = Nothing + +occNameToText :: OccName -> Text +occNameToText = Text.pack . occNameString + +updateColOffset :: Int -> Int -> Int -> Int +updateColOffset row lineOffset colOffset + | row == lineOffset = colOffset + | otherwise = 0 + +usedIdentifiersToTextEdits :: Range -> NameEnv [ImportedBy] -> Text -> [UsedIdentifier] -> [TextEdit] +usedIdentifiersToTextEdits range nameToImportedByMap sourceText usedIdentifiers + | let sortedUsedIdentifiers = sortOn usedIdentifierSpan usedIdentifiers = + State.evalState (makeStateComputation sortedUsedIdentifiers) (Text.lines sourceText, 0, 0) + where + folder :: [TextEdit] -> UsedIdentifier -> State ([Text], Int, Int) [TextEdit] + folder prevTextEdits (UsedIdentifier identifierName identifierSpan) + | Just importedBys <- lookupNameEnv nameToImportedByMap identifierName + , Just (ImportedBy alias _) <- find (isRangeWithinImportedBy range) importedBys + , let IdentifierSpan row startCol endCol = identifierSpan + , let identifierRange = identifierSpanToRange identifierSpan + , let aliasText = Text.pack $ moduleNameString alias + , let identifierText = Text.pack $ occNameString $ nameOccName identifierName + , let qualifiedIdentifierText = aliasText <> "." <> identifierText = do + (sourceTextLines, lineOffset, updateColOffset row lineOffset -> colOffset) <- State.get + let lines = List.drop (row - lineOffset) sourceTextLines + let (replacementText, remainingLines) = + if | line : remainingLines <- lines + , let lineStartingAtIdentifier = Text.drop (startCol - colOffset) line + , Just (c, _) <- Text.uncons lineStartingAtIdentifier + , let isParenthesized = c == '(' + , let isBackticked = c == '`' + , let replacementText = + if | isParenthesized -> "(" <> qualifiedIdentifierText <> ")" + | isBackticked -> "`" <> qualifiedIdentifierText <> "`" + | otherwise -> qualifiedIdentifierText -> + (replacementText, lineStartingAtIdentifier : remainingLines) + | otherwise -> (qualifiedIdentifierText, lines) + let textEdit = TextEdit identifierRange replacementText + State.put (remainingLines, row, startCol) + pure $ textEdit : prevTextEdits + | otherwise = pure prevTextEdits + + makeStateComputation :: [UsedIdentifier] -> State ([Text], Int, Int) [TextEdit] + makeStateComputation usedIdentifiers = foldM folder [] usedIdentifiers + +-- The overall idea: +-- 1. GlobalRdrEnv from typechecking phase contains info on what imported a +-- name. +-- 2. refMap from GetHieAst contains location of names and how they are used. +-- 3. For each used name in refMap check whether the name comes from an import +-- at the origin of the code action. +codeActionProvider :: PluginMethodHandler IdeState TextDocumentCodeAction +codeActionProvider ideState pluginId (CodeActionParams _ _ documentId range context) + | TextDocumentIdentifier uri <- documentId + , Just normalizedFilePath <- uriToNormalizedFilePath (toNormalizedUri uri) = liftIO $ do + tcModuleResult <- getTypeCheckedModule ideState normalizedFilePath + if | Just TcModuleResult { tmrParsed, tmrTypechecked } <- tcModuleResult + , Just _ <- findLImportDeclAt range tmrParsed -> do + hieAstResult <- getHieAst ideState normalizedFilePath + sourceText <- getSourceText ideState normalizedFilePath + if | Just HAR {..} <- hieAstResult + , Just sourceText <- sourceText + , let globalRdrEnv = tcg_rdr_env tmrTypechecked + , let nameToImportedByMap = globalRdrEnvToNameToImportedByMap globalRdrEnv + , let usedIdentifiers = refMapToUsedIdentifiers refMap + , let textEdits = usedIdentifiersToTextEdits range nameToImportedByMap sourceText usedIdentifiers -> + pure $ Right $ List (makeCodeActions uri textEdits) + | otherwise -> pure $ Right $ List [] + | otherwise -> pure $ Right $ List [] + | otherwise = pure $ Right $ List [] + diff --git a/plugins/hls-qualify-imported-names-plugin/test/Main.hs b/plugins/hls-qualify-imported-names-plugin/test/Main.hs new file mode 100644 index 0000000000..ac1c50a2cd --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/Main.hs @@ -0,0 +1,146 @@ +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeOperators #-} + +module Main (main) where + +import Data.Foldable (find) +import Data.Text (Text) +import qualified Ide.Plugin.QualifyImportedNames as QualifyImportedNames +import System.FilePath (()) +import Test.Hls (CodeAction (CodeAction, _title), + Command (Command), IdeState, + MonadIO (liftIO), + PluginDescriptor, + Position (Position), + Range (Range), Session, + TestName, TestTree, + TextDocumentIdentifier, + assertBool, assertFailure, + defaultTestRunner, + executeCodeAction, + getCodeActions, + goldenWithHaskellDoc, openDoc, + rename, runSessionWithServer, + testCase, testGroup, + type (|?) (InR), (@?=)) + +import Prelude + +-- 1's based +data Point = Point { + line :: !Int, + column :: !Int +} + +makePoint line column + | line >= 1 && column >= 1 = Point line column + | otherwise = error "Line or column is less than 1." + +isNotEmpty :: Foldable f => f a -> Bool +isNotEmpty = not . isEmpty + +isEmpty :: Foldable f => f a -> Bool +isEmpty = null + +makeCodeActionNotFoundAtString :: Point -> String +makeCodeActionNotFoundAtString Point {..} = + "CodeAction not found at line: " <> show line <> ", column: " <> show column + +makeCodeActionFoundAtString :: Point -> String +makeCodeActionFoundAtString Point {..} = + "CodeAction found at line: " <> show line <> ", column: " <> show column + +main :: IO () +main = defaultTestRunner $ testGroup "Qualify Imported Names" + [ + testCase "No CodeAction when not at import" $ + runSessionWithServer pluginDescriptor testDataDir $ do + let point = makePoint 1 1 + document <- openDoc "NoImport.hs" "haskell" + actions <- getCodeActions document $ pointToRange point + liftIO $ assertBool (makeCodeActionFoundAtString point) (isEmpty actions) + , testCase "No CodeAction when import is qualified" $ + runSessionWithServer pluginDescriptor testDataDir $ do + let point = makePoint 3 1 + document <- openDoc "QualifiedImport.hs" "haskell" + actions <- getCodeActions document $ pointToRange point + liftIO $ assertBool (makeCodeActionFoundAtString point) (isEmpty actions) + , codeActionGoldenTest + "CodeAction qualifies names with alias if imported module is aliased" + "AliasedImport" + (makePoint 3 1) + , codeActionGoldenTest + "CodeAction qualifies names with module name if imported module is not aliased" + "UnaliasedImport" + (makePoint 3 1) + , codeActionGoldenTest + "CodeAction qualifies only names in import's explicit non-hiding list" + "ExplicitImport" + (makePoint 4 1) + , codeActionGoldenTest + "CodeAction qualifies only names outside of import's explicit hiding list" + "ExplicitHidingImport" + (makePoint 4 1) + , codeActionGoldenTest + "CodeAction can qualify names not defined in modules they are imported from" + "Reexported" + (makePoint 3 1) + , codeActionGoldenTest + "CodeAction can qualify explicitly imported Prelude" + "ExplicitPrelude" + (makePoint 3 1) + , codeActionGoldenTest + "CodeAction qualifies only imported names" + "OnlyImportedNames" + (makePoint 3 1) + , codeActionGoldenTest + "CodeAction qualifies parenthesized operators properly" + "Parenthesized" + (makePoint 3 1) + , codeActionGoldenTest + "CodeAction qualifies backticked operators properly" + "Backticked" + (makePoint 3 1) + , codeActionGoldenTest + "CodeAction qualifies parenthesized and backticked operators on the same line properly" + "SameLine" + (makePoint 3 1) + , codeActionGoldenTest + "CodeAction doesn't qualify already qualified names" + "NoDoubleQualify" + (makePoint 3 1) + ] + +codeActionGoldenTest :: TestName -> FilePath -> Point -> TestTree +codeActionGoldenTest testCaseName goldenFilename point = + goldenWithQualifyImportedNames testCaseName goldenFilename $ \document -> do + actions <- getCodeActions document $ pointToRange point + case find ((== Just "Qualify imported names") . getCodeActionTitle) actions of + Just (InR codeAction) -> executeCodeAction codeAction + _ -> liftIO $ assertFailure $ makeCodeActionNotFoundAtString point + +testDataDir :: String +testDataDir = "test" "data" + +pluginDescriptor :: PluginDescriptor IdeState +pluginDescriptor = QualifyImportedNames.descriptor "qualifyImportedNames" + +getCodeActionTitle :: (Command |? CodeAction) -> Maybe Text +getCodeActionTitle commandOrCodeAction + | InR CodeAction {_title} <- commandOrCodeAction = Just _title + | otherwise = Nothing + +goldenWithQualifyImportedNames :: TestName -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree +goldenWithQualifyImportedNames testName path = + goldenWithHaskellDoc pluginDescriptor testName testDataDir path "expected" "hs" + +pointToRange :: Point -> Range +pointToRange Point {..} + | line <- subtract 1 line + , column <- subtract 1 column = + Range (Position line column) (Position line $ column + 1) + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/A.hs b/plugins/hls-qualify-imported-names-plugin/test/data/A.hs new file mode 100644 index 0000000000..d6ecd773cb --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/A.hs @@ -0,0 +1,12 @@ +module A (module B, a, b, op) where + +import B + +a :: Int -> Int +a = id + +b :: String -> String +b = id + +op :: Int -> Int -> Int +op = (+) diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/AliasedImport.expected.hs b/plugins/hls-qualify-imported-names-plugin/test/data/AliasedImport.expected.hs new file mode 100644 index 0000000000..406ab21f81 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/AliasedImport.expected.hs @@ -0,0 +1,6 @@ +module AliasedImport where + +import A as B + +thing = B.a + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/AliasedImport.hs b/plugins/hls-qualify-imported-names-plugin/test/data/AliasedImport.hs new file mode 100644 index 0000000000..92febc3dbd --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/AliasedImport.hs @@ -0,0 +1,6 @@ +module AliasedImport where + +import A as B + +thing = a + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/B.hs b/plugins/hls-qualify-imported-names-plugin/test/data/B.hs new file mode 100644 index 0000000000..78795f4a2e --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/B.hs @@ -0,0 +1,7 @@ +module B where + +c :: Int +c = 3 + +d :: String +d = "d" diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/Backticked.expected.hs b/plugins/hls-qualify-imported-names-plugin/test/data/Backticked.expected.hs new file mode 100644 index 0000000000..debea0794b --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/Backticked.expected.hs @@ -0,0 +1,10 @@ +module Backticked where + +import Prelude + +f a b = a `Prelude.elem` b + +g a b = + let h = f a b + in a `Prelude.elem` b + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/Backticked.hs b/plugins/hls-qualify-imported-names-plugin/test/data/Backticked.hs new file mode 100644 index 0000000000..981b46e84d --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/Backticked.hs @@ -0,0 +1,10 @@ +module Backticked where + +import Prelude + +f a b = a `elem` b + +g a b = + let h = f a b + in a `elem` b + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitHidingImport.expected.hs b/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitHidingImport.expected.hs new file mode 100644 index 0000000000..013d8fe630 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitHidingImport.expected.hs @@ -0,0 +1,8 @@ +module ExplicitHidingImport where + +import A +import A hiding (b) + +thing1 = A.a +thing2 = b + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitHidingImport.hs b/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitHidingImport.hs new file mode 100644 index 0000000000..41c3421e86 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitHidingImport.hs @@ -0,0 +1,8 @@ +module ExplicitHidingImport where + +import A +import A hiding (b) + +thing1 = a +thing2 = b + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitImport.expected.hs b/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitImport.expected.hs new file mode 100644 index 0000000000..bb18252868 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitImport.expected.hs @@ -0,0 +1,8 @@ +module ExplicitImport where + +import A (a) +import A (b) + +thing1 = a +thing2 = A.b + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitImport.hs b/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitImport.hs new file mode 100644 index 0000000000..3e2a7dba1e --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitImport.hs @@ -0,0 +1,8 @@ +module ExplicitImport where + +import A (a) +import A (b) + +thing1 = a +thing2 = b + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitPrelude.expected.hs b/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitPrelude.expected.hs new file mode 100644 index 0000000000..64e67291aa --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitPrelude.expected.hs @@ -0,0 +1,10 @@ +module ExplicitPrelude where + +import Prelude + +f :: Prelude.String -> Prelude.Int -> Prelude.Maybe Prelude.Bool +f a b = Prelude.Just Prelude.False + +class Prelude.Functor f => MyClass f where + method :: f Prelude.Int + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitPrelude.hs b/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitPrelude.hs new file mode 100644 index 0000000000..9fcdb9c5f9 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/ExplicitPrelude.hs @@ -0,0 +1,10 @@ +module ExplicitPrelude where + +import Prelude + +f :: String -> Int -> Maybe Bool +f a b = Just False + +class Functor f => MyClass f where + method :: f Int + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/NoDoubleQualify.expected.hs b/plugins/hls-qualify-imported-names-plugin/test/data/NoDoubleQualify.expected.hs new file mode 100644 index 0000000000..bf09b95ba2 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/NoDoubleQualify.expected.hs @@ -0,0 +1,7 @@ +module NoDoubleQualify where + +import A as AAA + +thing = AAA.a +thing2 = (AAA.op) +thing3 = 1 `AAA.op` 2 diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/NoDoubleQualify.hs b/plugins/hls-qualify-imported-names-plugin/test/data/NoDoubleQualify.hs new file mode 100644 index 0000000000..bf09b95ba2 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/NoDoubleQualify.hs @@ -0,0 +1,7 @@ +module NoDoubleQualify where + +import A as AAA + +thing = AAA.a +thing2 = (AAA.op) +thing3 = 1 `AAA.op` 2 diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/NoImport.hs b/plugins/hls-qualify-imported-names-plugin/test/data/NoImport.hs new file mode 100644 index 0000000000..9e7cac3885 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/NoImport.hs @@ -0,0 +1,4 @@ +module NoImport where + +f = 3 + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/OnlyImportedNames.expected.hs b/plugins/hls-qualify-imported-names-plugin/test/data/OnlyImportedNames.expected.hs new file mode 100644 index 0000000000..f0d7cec81d --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/OnlyImportedNames.expected.hs @@ -0,0 +1,16 @@ +module OnlyImportedNames where + +import A + +thing1 a = a + +thing2 b = b + +thing3 = f1 A.a A.c + +thing4 = f2 A.b A.d + +f1 a = a + +f2 c b = let { d = "k"; e = A.a } in c d ++ c b + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/OnlyImportedNames.hs b/plugins/hls-qualify-imported-names-plugin/test/data/OnlyImportedNames.hs new file mode 100644 index 0000000000..514a2f7eff --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/OnlyImportedNames.hs @@ -0,0 +1,16 @@ +module OnlyImportedNames where + +import A + +thing1 a = a + +thing2 b = b + +thing3 = f1 a c + +thing4 = f2 b d + +f1 a = a + +f2 c b = let { d = "k"; e = a } in c d ++ c b + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/Parenthesized.expected.hs b/plugins/hls-qualify-imported-names-plugin/test/data/Parenthesized.expected.hs new file mode 100644 index 0000000000..969f2a1a4f --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/Parenthesized.expected.hs @@ -0,0 +1,6 @@ +module Parenthesized where + +import Prelude + +thing :: [Prelude.Int] -> [Prelude.Int] -> [Prelude.Int] +thing = (Prelude.<>) diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/Parenthesized.hs b/plugins/hls-qualify-imported-names-plugin/test/data/Parenthesized.hs new file mode 100644 index 0000000000..f94e35f9a2 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/Parenthesized.hs @@ -0,0 +1,6 @@ +module Parenthesized where + +import Prelude + +thing :: [Int] -> [Int] -> [Int] +thing = (<>) diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/QualifiedImport.hs b/plugins/hls-qualify-imported-names-plugin/test/data/QualifiedImport.hs new file mode 100644 index 0000000000..d79756cd7b --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/QualifiedImport.hs @@ -0,0 +1,4 @@ +module QualifiedImport where + +import qualified A + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/Reexported.expected.hs b/plugins/hls-qualify-imported-names-plugin/test/data/Reexported.expected.hs new file mode 100644 index 0000000000..beb612266b --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/Reexported.expected.hs @@ -0,0 +1,6 @@ +module Reexported where + +import A + +thing = A.c + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/Reexported.hs b/plugins/hls-qualify-imported-names-plugin/test/data/Reexported.hs new file mode 100644 index 0000000000..55bd197c09 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/Reexported.hs @@ -0,0 +1,6 @@ +module Reexported where + +import A + +thing = c + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/SameLine.expected.hs b/plugins/hls-qualify-imported-names-plugin/test/data/SameLine.expected.hs new file mode 100644 index 0000000000..c772679c22 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/SameLine.expected.hs @@ -0,0 +1,5 @@ +module SameLine where + +import A + +thing = ((A.a) . (A.a) . (A.a)) (1 `A.op` 2 `A.op` 3 `A.op` 4) diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/SameLine.hs b/plugins/hls-qualify-imported-names-plugin/test/data/SameLine.hs new file mode 100644 index 0000000000..4a69baf238 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/SameLine.hs @@ -0,0 +1,5 @@ +module SameLine where + +import A + +thing = ((a) . (a) . (a)) (1 `op` 2 `op` 3 `op` 4) diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/UnaliasedImport.expected.hs b/plugins/hls-qualify-imported-names-plugin/test/data/UnaliasedImport.expected.hs new file mode 100644 index 0000000000..f24e555ad8 --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/UnaliasedImport.expected.hs @@ -0,0 +1,6 @@ +module UnaliasedImport where + +import A + +thing = A.a + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/UnaliasedImport.hs b/plugins/hls-qualify-imported-names-plugin/test/data/UnaliasedImport.hs new file mode 100644 index 0000000000..c7fc8eec0a --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/UnaliasedImport.hs @@ -0,0 +1,6 @@ +module UnaliasedImport where + +import A + +thing = a + diff --git a/plugins/hls-qualify-imported-names-plugin/test/data/hie.yaml b/plugins/hls-qualify-imported-names-plugin/test/data/hie.yaml new file mode 100644 index 0000000000..a3f448957c --- /dev/null +++ b/plugins/hls-qualify-imported-names-plugin/test/data/hie.yaml @@ -0,0 +1,17 @@ +cradle: + direct: + arguments: + - A.hs + - B.hs + - QualifiedImport.hs + - NoImport.hs + - AliasedImport.hs + - UnaliasedImport.hs + - ExplicitImport.hs + - ExplicitHidingImport.hs + - Reexported.hs + - ExplicitPrelude.hs + - OnlyImportedNames.hs + - Parenthesized.hs + - Backticked.hs + diff --git a/stack-8.10.5.yaml b/stack-8.10.5.yaml index f94d4f74ef..cf9d3a1f56 100644 --- a/stack-8.10.5.yaml +++ b/stack-8.10.5.yaml @@ -21,6 +21,7 @@ packages: - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin + - ./plugins/hls-qualify-imported-names-plugin - ./plugins/hls-brittany-plugin - ./plugins/hls-stylish-haskell-plugin - ./plugins/hls-floskell-plugin diff --git a/stack-8.10.6.yaml b/stack-8.10.6.yaml index fafa2f08b0..12647fa241 100644 --- a/stack-8.10.6.yaml +++ b/stack-8.10.6.yaml @@ -18,6 +18,7 @@ packages: - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin + - ./plugins/hls-qualify-imported-names-plugin - ./plugins/hls-brittany-plugin - ./plugins/hls-stylish-haskell-plugin - ./plugins/hls-floskell-plugin diff --git a/stack-8.10.7.yaml b/stack-8.10.7.yaml index 04e7adf902..2e978f410e 100644 --- a/stack-8.10.7.yaml +++ b/stack-8.10.7.yaml @@ -18,6 +18,7 @@ packages: - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin + - ./plugins/hls-qualify-imported-names-plugin - ./plugins/hls-brittany-plugin - ./plugins/hls-stylish-haskell-plugin - ./plugins/hls-floskell-plugin diff --git a/stack-8.6.5.yaml b/stack-8.6.5.yaml index 9f9532f626..837fa58f3f 100644 --- a/stack-8.6.5.yaml +++ b/stack-8.6.5.yaml @@ -19,6 +19,7 @@ packages: - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin + - ./plugins/hls-qualify-imported-names-plugin - ./plugins/hls-brittany-plugin - ./plugins/hls-stylish-haskell-plugin - ./plugins/hls-floskell-plugin diff --git a/stack-8.8.3.yaml b/stack-8.8.3.yaml index 543bf2edd9..bad564053e 100644 --- a/stack-8.8.3.yaml +++ b/stack-8.8.3.yaml @@ -19,6 +19,7 @@ packages: - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin + - ./plugins/hls-qualify-imported-names-plugin - ./plugins/hls-brittany-plugin - ./plugins/hls-stylish-haskell-plugin - ./plugins/hls-floskell-plugin diff --git a/stack-8.8.4.yaml b/stack-8.8.4.yaml index 0bc74c05e2..367d2ed7d7 100644 --- a/stack-8.8.4.yaml +++ b/stack-8.8.4.yaml @@ -19,6 +19,7 @@ packages: - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-tactics-plugin + - ./plugins/hls-qualify-imported-names-plugin - ./plugins/hls-brittany-plugin - ./plugins/hls-stylish-haskell-plugin - ./plugins/hls-floskell-plugin diff --git a/stack-9.0.1.yaml b/stack-9.0.1.yaml index a34cd671a8..21ca0b2827 100644 --- a/stack-9.0.1.yaml +++ b/stack-9.0.1.yaml @@ -13,6 +13,7 @@ packages: - ./plugins/hls-haddock-comments-plugin - ./plugins/hls-eval-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-qualify-imported-names-plugin - ./plugins/hls-refine-imports-plugin - ./plugins/hls-hlint-plugin - ./plugins/hls-rename-plugin diff --git a/stack.yaml b/stack.yaml index f38265d1f5..dffcf006fb 100644 --- a/stack.yaml +++ b/stack.yaml @@ -17,6 +17,7 @@ packages: - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin + - ./plugins/hls-qualify-imported-names-plugin - ./plugins/hls-tactics-plugin - ./plugins/hls-brittany-plugin - ./plugins/hls-stylish-haskell-plugin