smuggler2 (empty) → 0.3.2.1
raw patch · 40 files changed
+1784/−0 lines, 40 filesdep +basedep +containersdep +directorysetup-changed
Dependencies added: base, containers, directory, filepath, ghc, ghc-exactprint, ghc-paths, smuggler2, syb, tasty, tasty-golden, text, typed-process
Files
- CHANGELOG.md +16/−0
- LICENSE +373/−0
- Makefile +28/−0
- README.md +309/−0
- Setup.hs +2/−0
- TODO.md +14/−0
- app/Main.hs +13/−0
- smuggler2.cabal +145/−0
- src/Smuggler2/Anns.hs +171/−0
- src/Smuggler2/Options.hs +50/−0
- src/Smuggler2/Parser.hs +48/−0
- src/Smuggler2/Plugin.hs +300/−0
- test/Test.hs +113/−0
- test/tests/Bare.hs +4/−0
- test/tests/BareHello.hs +3/−0
- test/tests/BoolBoolUnused.hs +3/−0
- test/tests/BoolBoolUsed.hs +5/−0
- test/tests/BoolPartlyUsedFirst.hs +5/−0
- test/tests/BoolPartlyUsedLast.hs +5/−0
- test/tests/Class.hs +42/−0
- test/tests/ConstructorsUnused.hs +5/−0
- test/tests/Dummy.hs +4/−0
- test/tests/Duplicated.hs +8/−0
- test/tests/ExportExplicit.hs +10/−0
- test/tests/ExportImplicit.hs +10/−0
- test/tests/ImplicitImportUnused.hs +9/−0
- test/tests/ImplicitImportUsed.hs +5/−0
- test/tests/ImportEmpty.hs +3/−0
- test/tests/MoreExports.hs +20/−0
- test/tests/MultilineUnused.hs +10/−0
- test/tests/Operator.hs +8/−0
- test/tests/QualUnqual.hs +6/−0
- test/tests/TypeConstructorUnused.hs +5/−0
- test/tests/TypeConstructorUnusedComma.hs +5/−0
- test/tests/TypeConstructorUsed.hs +5/−0
- test/tests/TypeUnused.hs +5/−0
- test/tests/TypeUsed.hs +6/−0
- test/tests/WildcardUnused.hs +5/−0
- test/tests/WildcardUsed.hs +5/−0
- weeder.dhall +1/−0
+ CHANGELOG.md view
@@ -0,0 +1,16 @@+# Changelog++Notable changes to `Smuggler2`++`smuggler2` uses [PVP Versioning][1].+The change log is available [on GitHub][2].++## [Unreleased]+- Nothing to report++## [0.3.2.1]: -- 30 May 2020+- Provisional version for Hackage+++[1]: https://pvp.haskell.org+[2]: https://github.com/jrp2014/smuggler2/releases
+ LICENSE view
@@ -0,0 +1,373 @@+Mozilla Public License Version 2.0+==================================++1. Definitions+--------------++1.1. "Contributor"+ means each individual or legal entity that creates, contributes to+ the creation of, or owns Covered Software.++1.2. "Contributor Version"+ means the combination of the Contributions of others (if any) used+ by a Contributor and that particular Contributor's Contribution.++1.3. "Contribution"+ means Covered Software of a particular Contributor.++1.4. "Covered Software"+ means Source Code Form to which the initial Contributor has attached+ the notice in Exhibit A, the Executable Form of such Source Code+ Form, and Modifications of such Source Code Form, in each case+ including portions thereof.++1.5. "Incompatible With Secondary Licenses"+ means++ (a) that the initial Contributor has attached the notice described+ in Exhibit B to the Covered Software; or++ (b) that the Covered Software was made available under the terms of+ version 1.1 or earlier of the License, but not also under the+ terms of a Secondary License.++1.6. "Executable Form"+ means any form of the work other than Source Code Form.++1.7. "Larger Work"+ means a work that combines Covered Software with other material, in+ a separate file or files, that is not Covered Software.++1.8. "License"+ means this document.++1.9. "Licensable"+ means having the right to grant, to the maximum extent possible,+ whether at the time of the initial grant or subsequently, any and+ all of the rights conveyed by this License.++1.10. "Modifications"+ means any of the following:++ (a) any file in Source Code Form that results from an addition to,+ deletion from, or modification of the contents of Covered+ Software; or++ (b) any new file in Source Code Form that contains any Covered+ Software.++1.11. "Patent Claims" of a Contributor+ means any patent claim(s), including without limitation, method,+ process, and apparatus claims, in any patent Licensable by such+ Contributor that would be infringed, but for the grant of the+ License, by the making, using, selling, offering for sale, having+ made, import, or transfer of either its Contributions or its+ Contributor Version.++1.12. "Secondary License"+ means either the GNU General Public License, Version 2.0, the GNU+ Lesser General Public License, Version 2.1, the GNU Affero General+ Public License, Version 3.0, or any later versions of those+ licenses.++1.13. "Source Code Form"+ means the form of the work preferred for making modifications.++1.14. "You" (or "Your")+ means an individual or a legal entity exercising rights under this+ License. For legal entities, "You" includes any entity that+ controls, is controlled by, or is under common control with You. For+ purposes of this definition, "control" means (a) the power, direct+ or indirect, to cause the direction or management of such entity,+ whether by contract or otherwise, or (b) ownership of more than+ fifty percent (50%) of the outstanding shares or beneficial+ ownership of such entity.++2. License Grants and Conditions+--------------------------------++2.1. Grants++Each Contributor hereby grants You a world-wide, royalty-free,+non-exclusive license:++(a) under intellectual property rights (other than patent or trademark)+ Licensable by such Contributor to use, reproduce, make available,+ modify, display, perform, distribute, and otherwise exploit its+ Contributions, either on an unmodified basis, with Modifications, or+ as part of a Larger Work; and++(b) under Patent Claims of such Contributor to make, use, sell, offer+ for sale, have made, import, and otherwise transfer either its+ Contributions or its Contributor Version.++2.2. Effective Date++The licenses granted in Section 2.1 with respect to any Contribution+become effective for each Contribution on the date the Contributor first+distributes such Contribution.++2.3. Limitations on Grant Scope++The licenses granted in this Section 2 are the only rights granted under+this License. No additional rights or licenses will be implied from the+distribution or licensing of Covered Software under this License.+Notwithstanding Section 2.1(b) above, no patent license is granted by a+Contributor:++(a) for any code that a Contributor has removed from Covered Software;+ or++(b) for infringements caused by: (i) Your and any other third party's+ modifications of Covered Software, or (ii) the combination of its+ Contributions with other software (except as part of its Contributor+ Version); or++(c) under Patent Claims infringed by Covered Software in the absence of+ its Contributions.++This License does not grant any rights in the trademarks, service marks,+or logos of any Contributor (except as may be necessary to comply with+the notice requirements in Section 3.4).++2.4. Subsequent Licenses++No Contributor makes additional grants as a result of Your choice to+distribute the Covered Software under a subsequent version of this+License (see Section 10.2) or under the terms of a Secondary License (if+permitted under the terms of Section 3.3).++2.5. Representation++Each Contributor represents that the Contributor believes its+Contributions are its original creation(s) or it has sufficient rights+to grant the rights to its Contributions conveyed by this License.++2.6. Fair Use++This License is not intended to limit any rights You have under+applicable copyright doctrines of fair use, fair dealing, or other+equivalents.++2.7. Conditions++Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted+in Section 2.1.++3. Responsibilities+-------------------++3.1. Distribution of Source Form++All distribution of Covered Software in Source Code Form, including any+Modifications that You create or to which You contribute, must be under+the terms of this License. You must inform recipients that the Source+Code Form of the Covered Software is governed by the terms of this+License, and how they can obtain a copy of this License. You may not+attempt to alter or restrict the recipients' rights in the Source Code+Form.++3.2. Distribution of Executable Form++If You distribute Covered Software in Executable Form then:++(a) such Covered Software must also be made available in Source Code+ Form, as described in Section 3.1, and You must inform recipients of+ the Executable Form how they can obtain a copy of such Source Code+ Form by reasonable means in a timely manner, at a charge no more+ than the cost of distribution to the recipient; and++(b) You may distribute such Executable Form under the terms of this+ License, or sublicense it under different terms, provided that the+ license for the Executable Form does not attempt to limit or alter+ the recipients' rights in the Source Code Form under this License.++3.3. Distribution of a Larger Work++You may create and distribute a Larger Work under terms of Your choice,+provided that You also comply with the requirements of this License for+the Covered Software. If the Larger Work is a combination of Covered+Software with a work governed by one or more Secondary Licenses, and the+Covered Software is not Incompatible With Secondary Licenses, this+License permits You to additionally distribute such Covered Software+under the terms of such Secondary License(s), so that the recipient of+the Larger Work may, at their option, further distribute the Covered+Software under the terms of either this License or such Secondary+License(s).++3.4. Notices++You may not remove or alter the substance of any license notices+(including copyright notices, patent notices, disclaimers of warranty,+or limitations of liability) contained within the Source Code Form of+the Covered Software, except that You may alter any license notices to+the extent required to remedy known factual inaccuracies.++3.5. Application of Additional Terms++You may choose to offer, and to charge a fee for, warranty, support,+indemnity or liability obligations to one or more recipients of Covered+Software. However, You may do so only on Your own behalf, and not on+behalf of any Contributor. You must make it absolutely clear that any+such warranty, support, indemnity, or liability obligation is offered by+You alone, and You hereby agree to indemnify every Contributor for any+liability incurred by such Contributor as a result of warranty, support,+indemnity or liability terms You offer. You may include additional+disclaimers of warranty and limitations of liability specific to any+jurisdiction.++4. Inability to Comply Due to Statute or Regulation+---------------------------------------------------++If it is impossible for You to comply with any of the terms of this+License with respect to some or all of the Covered Software due to+statute, judicial order, or regulation then You must: (a) comply with+the terms of this License to the maximum extent possible; and (b)+describe the limitations and the code they affect. Such description must+be placed in a text file included with all distributions of the Covered+Software under this License. Except to the extent prohibited by statute+or regulation, such description must be sufficiently detailed for a+recipient of ordinary skill to be able to understand it.++5. Termination+--------------++5.1. The rights granted under this License will terminate automatically+if You fail to comply with any of its terms. However, if You become+compliant, then the rights granted under this License from a particular+Contributor are reinstated (a) provisionally, unless and until such+Contributor explicitly and finally terminates Your grants, and (b) on an+ongoing basis, if such Contributor fails to notify You of the+non-compliance by some reasonable means prior to 60 days after You have+come back into compliance. Moreover, Your grants from a particular+Contributor are reinstated on an ongoing basis if such Contributor+notifies You of the non-compliance by some reasonable means, this is the+first time You have received notice of non-compliance with this License+from such Contributor, and You become compliant prior to 30 days after+Your receipt of the notice.++5.2. If You initiate litigation against any entity by asserting a patent+infringement claim (excluding declaratory judgment actions,+counter-claims, and cross-claims) alleging that a Contributor Version+directly or indirectly infringes any patent, then the rights granted to+You by any and all Contributors for the Covered Software under Section+2.1 of this License shall terminate.++5.3. In the event of termination under Sections 5.1 or 5.2 above, all+end user license agreements (excluding distributors and resellers) which+have been validly granted by You or Your distributors under this License+prior to termination shall survive termination.++************************************************************************+* *+* 6. Disclaimer of Warranty *+* ------------------------- *+* *+* Covered Software is provided under this License on an "as is" *+* basis, without warranty of any kind, either expressed, implied, or *+* statutory, including, without limitation, warranties that the *+* Covered Software is free of defects, merchantable, fit for a *+* particular purpose or non-infringing. The entire risk as to the *+* quality and performance of the Covered Software is with You. *+* Should any Covered Software prove defective in any respect, You *+* (not any Contributor) assume the cost of any necessary servicing, *+* repair, or correction. This disclaimer of warranty constitutes an *+* essential part of this License. No use of any Covered Software is *+* authorized under this License except under this disclaimer. *+* *+************************************************************************++************************************************************************+* *+* 7. Limitation of Liability *+* -------------------------- *+* *+* Under no circumstances and under no legal theory, whether tort *+* (including negligence), contract, or otherwise, shall any *+* Contributor, or anyone who distributes Covered Software as *+* permitted above, be liable to You for any direct, indirect, *+* special, incidental, or consequential damages of any character *+* including, without limitation, damages for lost profits, loss of *+* goodwill, work stoppage, computer failure or malfunction, or any *+* and all other commercial damages or losses, even if such party *+* shall have been informed of the possibility of such damages. This *+* limitation of liability shall not apply to liability for death or *+* personal injury resulting from such party's negligence to the *+* extent applicable law prohibits such limitation. Some *+* jurisdictions do not allow the exclusion or limitation of *+* incidental or consequential damages, so this exclusion and *+* limitation may not apply to You. *+* *+************************************************************************++8. Litigation+-------------++Any litigation relating to this License may be brought only in the+courts of a jurisdiction where the defendant maintains its principal+place of business and such litigation shall be governed by laws of that+jurisdiction, without reference to its conflict-of-law provisions.+Nothing in this Section shall prevent a party's ability to bring+cross-claims or counter-claims.++9. Miscellaneous+----------------++This License represents the complete agreement concerning the subject+matter hereof. If any provision of this License is held to be+unenforceable, such provision shall be reformed only to the extent+necessary to make it enforceable. Any law or regulation which provides+that the language of a contract shall be construed against the drafter+shall not be used to construe this License against a Contributor.++10. Versions of the License+---------------------------++10.1. New Versions++Mozilla Foundation is the license steward. Except as provided in Section+10.3, no one other than the license steward has the right to modify or+publish new versions of this License. Each version will be given a+distinguishing version number.++10.2. Effect of New Versions++You may distribute the Covered Software under the terms of the version+of the License under which You originally received the Covered Software,+or under the terms of any subsequent version published by the license+steward.++10.3. Modified Versions++If you create software not governed by this License, and you want to+create a new license for such software, you may create and use a+modified version of this License if you rename the license and remove+any references to the name of the license steward (except to note that+such modified license differs from this License).++10.4. Distributing Source Code Form that is Incompatible With Secondary+Licenses++If You choose to distribute Source Code Form that is Incompatible With+Secondary Licenses under the terms of this version of the License, the+notice described in Exhibit B of this License must be attached.++Exhibit A - Source Code Form License Notice+-------------------------------------------++ This Source Code Form is subject to the terms of the Mozilla Public+ License, v. 2.0. If a copy of the MPL was not distributed with this+ file, You can obtain one at http://mozilla.org/MPL/2.0/.++If it is not possible or desirable to put the notice in a particular+file, then You may include the notice in a location (such as a LICENSE+file in a relevant directory) where a recipient would be likely to look+for such a notice.++You may add additional accurate notices of copyright ownership.++Exhibit B - "Incompatible With Secondary Licenses" Notice+---------------------------------------------------------++ This Source Code Form is "Incompatible With Secondary Licenses", as+ defined by the Mozilla Public License, v. 2.0.
+ Makefile view
@@ -0,0 +1,28 @@+# For convenience+#++.PHONY: test clean accept doc hlint ghcid weed++all: test doc++test:+ cabal test --test-show-details=streaming++clean:+ cabal clean++accept:+ cabal run smuggler2-test -- --accept++doc:+ cabal haddock++hlint:+ hlint src test/Test.hs++ghcid:+ ghcid++weed:+ cabal build+ weeder
+ README.md view
@@ -0,0 +1,309 @@+# smuggler2++<!--+[](https://hackage.haskell.org/package/smuggler2)+[](http://travis-ci.org/kowainik/smuggler)+-->++[](https://github.com/jrp2014/smuggler2/blob/master/LICENSE)++[](https://travis-ci.com/jrp2014/smuggler2)++Smuggler2 is a Haskell GHC Source Plugin that rewrites module imports (to produce a+minimal set) and adds or replaces explicit exports automatically.++This may make code easier to read because the provenance of imported+names is explcit. While writing code, it may be convenient to import a complete+module (by not specifiying what is to be imported from it) and then get+Smuggler2 to limit the import to include only the names that are used.++By default, all values, types and classes defined in a module+are exported (excluding those that are imported). Smuggler2 can generate+the code for that maximalist export list, for hand pruning. (It does not check+whether an exported name is used.) Limiting exports may make it easier for+`ghc` to optimise some code.++## How to use++### Adding Smuggler2 to your dependencies++Add `smuggler2` to the dependencies of your project and to your compiler+flags. For example, you could include in your project `cabal` file something+like++```Cabal+flag smuggler2+ description: Rewrite sources to cleanp imports, and create explicit exports+ exports+ default: False+ manual: True++common smuggler-options+ if flag(smuggler2)+ ghc-options: -fplugin:Smuggler2.Plugin+ build-depends: smuggler2 >= 0.3+```++and the `import: smuggler-options` in the appropriate `library` or `executable` sections.++The use of the flag allows you to build with or without source processing. Eg,++```bash+$ cabal build -fsmuggler2+```++using the example above.++You might use this approach to refine your imports or get a starting point for your+exports, but not rewrite them every time you compile. The use of a flag means+that you can also exclude `smuggler2` dependencies from your final builds.++### Alternatively, using a local version++If you have installed `smuggler2` from a local copy of this repository,+you may only need to add `-package smuggler2` to your `ghc-options`.++```Cabal+common smuggler-options+ if flag(smuggler2)+ ghc-options: -fplugin:Smuggler2.Plugin --package smuggler22+```++(You may need to install from a local copy using `cabal v1-install` for `smuggler2`+to be recognised.)++### Options++`Smuggler2` has several (case-insensitive) options, which can be set by adding a+`-fplugin-opt=Smuggler2.Plugin:` to your `ghc-options`++- `NoImportProcessing` - do no import processing+- `PreserveInstanceImports` - remove unused imports, but preserve a library import stub.+ such as `import Mod ()`, to import only instances of typeclasses from it. (The default.)+- `MinimiseImports` - remove unused imports, including any that may be needed+ only to import typeclass instances. This may, therefore, stop the module from compiling.++- `NoExportProcessing` - do no export processing+- `AddExplicitExports` - add an explicit list of all available exports (excluding+ those that are imported) if there is no existing export list. (The default.)+ You may want to edit it to keep specific values, types or classes local to the module.+ At present, a typeclass and its class methods are exported individually. You may want to+ replace those exports with an abbreviation such as `C(..)`.+- `ReplaceExports` - replace any existing module export list with one containing all+ available exports (which, again, you can, of course, then prune to your requirements).++Any other option value is used to generate a source file with a new extension of+the option value (`new` in the following example) rather than replacing the original file.++```Cabal+ ghc-options: -fplugin:Smuggler2.Plugin -fplugin-opt=Smuggler2.Plugin:new+```++This will create output files with a `.new` suffix rather the overwriting the originals.++Smuggler2 tries not to perform file changes when+there are no unused imports or exports to be added or replaced.+So you can just run `ghcid` as usual:++```bash+ghcid --command='cabal repl'+```++If you add `-v` to your `ghc-options`++## Caveats++`Smugggler2` is robust -- it can chew through the+[agda](https://github.com/agda/agda) codebase of over 370 modules with complex+interdependencies and be tripped over by only+- a handful of pattern synonym imports,+- a couple of ambiguous exports (are we trying to export something+defined in the current module or something with the same name from an imported+module)+- and a couple of imports where both qualifed and unqualifed version of the+ module are imported and there are references to both qualified and unqualifed+ version of the same names++But there are some caveats, most of which are either easy enough to work around+(and still benefit from a great reduction in keyboard work):+- `Smuggler2` rewrites the existing imports, rather than attempting to prune+ them. (This is a more aggressive approach than `smuggler` which focuses on+ removing redundant imports.) It has advantages and disadvantages. The+ advantage is that a minimal set of imports is generated in a reproducable format.+ The disdvantage is that imports may be reordered, comments and blankdropped, external+ imports mixed with external, etc.++- By default `Smuggler2` does not remove imports completely because an import may be being+ used to only import instances of typeclasses, So it will leave stubs like++ ```haskell+ import Mod ()+ ```++ that you may want to remove manually. Alternatively use the `MinimiseImports` option to+ remove them anyway, at the risk of producing code that fails to compile.++- CPP files will not be processed correctly: the imports will be generated for+ current CPP settings and any CPP annotations in the import block will be+ discarded. This may be a particular problem if you are writing code for+ several generations of `ghc` and `base` for example.+ [retrie](https://github.com/facebookincubator/retrie/blob/master/Retrie/CPP.hs)+ solves this problem generating all possible versions of the module+ (exponential in the number of `#if` directives), operating on each version+ individually, and splicing results back into the original file. A tour de+ force!++- `smuggler2` depends on the current `ghc` compiler and `base` library to check+ whether an import is redundant. Earlier versions of the compiler may, of+ course, need it. The [base library+ changelog](https://hackage.haskell.org/package/base/changelog) provides some+ details of what was made available when.++- Multiple separate import lines referring to the same library are not+ consolidated++- Literate Haskell `lhs` files are not supported++- `hiding` clauses may not be properly analysed. So hiding things that are not+ used may not be spotted.++- Certain syntax pattern imports may not be imported correctly (the `pattern`+ keyword is missing)++- The test suite does not seem to run reliably on Windows. This is probably+ more of an issue with the way that the tests are run, than `Smuggler2` itself.++- Currently `cabal` does not have a particular way of specifying plugins.+ (See, eg, https://gitlab.haskell.org/ghc/ghc/issues/11244 and+ https://github.com/haskell/cabal/issues/2965) which would allow cleaner+ separation of user code and plugin-code++## For contributors++Requirements:++- `ghc-8.6.5`, `ghc-8.8.3` and `ghc-8.10.1`: `Smuggler2` will not compile with earlier versions.+- The test golden values are for `ghc-8.10.1` and `ghc-8.8.3`. Some of them fail on `ghc-8.6.5`+ because it seems to need to import `Data.Bool` whereas later versions of GHC don't. The results+ compile on `ghc-8.6.5` and later anyway, but the imports are not as minimal+ for later versions as they could be.+- `cabal >= 3.0` (ideally `3.2`)++### How to build++```shell+$ cabal update+$ cabal build+```++To build with debugging:++```shell+$ cabal bulid -fdebug+```++Curently this just adds an `-fdump-minimal-imports` parameter to GHC+compilation.++### How to run tests++There is a `tasty-golden`-based test suite that can be run by++```shell+$ cabal test smuggler-test --enable-tests+```++Further help can be found by++```shell+$ cabal run smuggler-test -- --help+```++(note the extra `--`)++For example, if you are running on `ghc-8.6.5` you can++```shell+$ cabal run smuggler2-test -- --accept+```++to update the golden outputs to the current results of (failing) tests.++It is sometimes necessary to run `cabal clean` before running tests to ensure+that old build artefacts do not lead to misleading results.++`smuggler-test` uses `cabal exec ghc` internally to run a test. The `cabal` command+that is to be used to do that can be set using the `CABAL` environment variable.+This may be helpful for certain workflows where `cabal` is not in the current+path, or you want to add extra flags to the `cabal` command.++The test suite does not run reliably on Windows++## Implementation approach++`smuggler2` uses the `ghc-exactprint`+[library](https://hackage.haskell.org/package/ghc-exactprint) to modiify the+source code. The documentation for the library is fairly spartan, and the+library is not widely used, so the use here can, no doubt, be optimised.++The library is needed because the annotated AST that GHC generates does not have enough+information to reconstitute the original source.+Some parts of the renamed syntax tree (for example, imports) are+not found in the typechecked one. `ghc-exactprint` provides parsers that+preserve this information, which is stored in a separate+`Anns` `Map` used to generate properly formatted source text.++To make manipulation of GHC's AST and `ghc-exactprint`'s `Anns` easier,+`ghc-exactprint` provides a set of Transform functions. These are intended to facilitate+making changes to the AST and adjusting the `Anns` to suit the changes.++> These functions are [said to be under heavy development](https://hackage.haskell.org/package/ghc-exactprint-0.6.3/docs/Language-Haskell-GHC-ExactPrint-Transform.html).+> It is not entirely obvious how they are intended to be used or composed. The+> approach provided by [`retrie`](https://hackage.haskell.org/package/retrie)+> wraps an AST and `Anns` into a single type that seems to make AST+> transformations easier to compose and reduces the risk of the `Anns` and AST getting+> out of sync as it is being transformed, something with which the type system doesn't+> help you since the `Anns` are stored as a `Map`.++### Imports++`smuggler2` uses GHC to generate a set of minimal imports. It++- parses the original file+- dumps the minimal exports that GHC generates and parses them back in (to pick+ up the annotations needed for printing)+- drops implicit imports (such as Prelude) and, optionally, imports that are+ for instances only+- replaces the original imports with minimal ones+- `exactPrint`s the result back over the original file (or one with a different+ suffix, if that was specified as option to `smuggler2`)++This round tripping is needed because the AST provided by GHC to `smuggler2`+is of a different type from the AST that `ghc-exactprint` uses. (It is the+product of the renaming phase of the compiler, while `ghc-exactprint` uses+a parse phase AST.)++### Exports++Exports are simpler to deal with as GHC's `exports_from_avail` does the work.++## Other projects++- Smuggler2 was is a rewrite of [`smuggler`](https://hackage.haskell.org/package/smuggler)+- `retrie` a [code modding tool](https://hackage.haskell.org/package/retrie)+ that works with GHC 8.10.1+- `refact-global-hse` an ambitious [import refactoring tool](https://github.com/ddssff/refact-global-hse).+ This uses `haskell-src-exts` rather than `ghc-exactprint` and so may not work with current versions of GHC.+- These blog posts contain some fragments on the topic of using `ghc-exactprint` to manipulate import lists+ [Terser import declarations](https://www.machinesung.com/scribbles/terser-import-declarations.html) and+ [GHC API](https://www.machinesung.com/scribbles/ghc-api.html) (The site+ doesn't always seem to be up.)++## Acknowledgements++Thanks to++- Dmitrii Kovanikov and Veronika Romashkina who wrote [`smuggler`](https://hackage.haskell.org/package/smuggler)+- Alan Zimmerman and Matthew Pickering for+ [`ghc-exactprint`](https://hackage.haskell.org/package/ghc-exactprint)+- The ghc authors who have made the compiler internals available through an API.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ TODO.md view
@@ -0,0 +1,14 @@+# Things to be done++- [ ] does hiding work?++- [ ] does CPP work? (should do)++- [ ] Script to generate new test (from template?)++- [ ] get tasty-golden to delete successful results and update tests where+ smuggler makes no changes++- [ ] refactor the much more sophisticated https://github.com/ddssff/refact-global-hse++- [ ] Do a better job of preserving comments
+ app/Main.hs view
@@ -0,0 +1,13 @@+module Main (+ main ) where++import Data.Bool ( bool )+import Data.List ()+import Data.Maybe ( fromMaybe )++main :: IO ()+main = do+ let foo = fromMaybe bool Nothing+ print (foo undefined undefined undefined :: Int)+ print True+-- main = parseFile
+ smuggler2.cabal view
@@ -0,0 +1,145 @@+cabal-version: 3.0+name: smuggler2+version: 0.3.2.1+synopsis:+ GHC Source Plugin that helps to minimise imports and generate explicit exports++description:+ == Usage+ Add @smuggler2@ to the build dependencies of your project.+ .+ Then add the following to ghc-options: @-fplugin=Smuggler2.Plugin@++homepage: https://github.com/jrp2014/smuggler2+bug-reports: https://github.com/jrp2014/smuggler2+license: MPL-2.0+license-file: LICENSE+author: jrp2014, Dmitrii Kovanikov, Veronika Romashkina+maintainer: jrp2014+copyright: 2020 jrp2014, Dmitrii Kovanikov+category: Development, Refactoring, Compiler Plugin+build-type: Simple+extra-doc-files:+ README.md+ CHANGELOG.md+ TODO.md++extra-source-files:+ test/tests/*.hs+ Makefile+ Setup.hs+ weeder.dhall++-- wildcards are allowed only in the basename+--test/tests/*.*-golden++tested-with: GHC ==8.6.5 || ==8.8.3 || ==8.10.1++flag debug+ description: Enable debugging support+ default: False+ manual: True++flag threaded+ description: Build with support for multithreaded execution+ default: True+ manual: True++source-repository head+ type: git+ location: https://github.com/jrp2014/smuggler++common common-options+ -- test these bounds+ build-depends: base >=4.9 && <4.16++ -- -O2 gives a nearly 10% speed improvement+ ghc-options:+ -O2 -Wall -Wextra -Wincomplete-uni-patterns+ -Wincomplete-record-updates -Wcompat -Widentities+ -Wredundant-constraints -fhide-source-paths++ if impl(ghc >=8.10.0)+ ghc-options: -Wunused-packages -fwrite-ide-info++ if flag(debug)+ ghc-options: -ddump-minimal-imports+ cpp-options: -DDEBUG++ default-language: Haskell2010++ -- Most of these are unnecessary+ default-extensions:+ DeriveGeneric+ GeneralizedNewtypeDeriving+ InstanceSigs+ LambdaCase+ OverloadedStrings+ RecordWildCards+ ScopedTypeVariables+ StandaloneDeriving+ TypeApplications++common executable-options+ ghc-options: -rtsopts++ if flag(debug)+ ghc-options: -debug++ if flag(threaded)+ ghc-options: -threaded -with-rtsopts=-N++library+ import: common-options+ hs-source-dirs: src+ exposed-modules:+ Smuggler2.Options+ Smuggler2.Anns+ Smuggler2.Parser+ Smuggler2.Plugin++ other-modules:+ Paths_smuggler2++ autogen-modules: Paths_smuggler2++ build-depends:+ , containers ^>=0.6.0+ , directory ^>=1.3.3+ , filepath ^>=1.4.2+ , ghc >=8.6.5 && <8.11+ , ghc-exactprint ^>=0.6.3+ , syb ^>=0.7.1++ if flag(debug)+ build-depends: text++-- Currently doesn't do anything much+executable play-smuggler2+ import: common-options+ import: executable-options+ hs-source-dirs: app+ main-is: Main.hs++ if flag(debug)+ ghc-options: -fplugin=Smuggler2.Plugin++ build-depends: smuggler2++test-suite smuggler2-test+ import: common-options+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ build-depends:+ , filepath+ , ghc-paths+ , smuggler2+ , tasty+ , tasty-golden+ , typed-process++ main-is: Test.hs+ ghc-options: -rtsopts++ if flag(threaded)+ ghc-options: -threaded -with-rtsopts=-N
+ src/Smuggler2/Anns.hs view
@@ -0,0 +1,171 @@+{-|+ Description: Utility functions for transforming and manipulating 'ghc' AST elements+ and their associated 'ghc-exactprint' 'Language.Haskell.GHC.ExactPrint.Anns'+ -}+module Smuggler2.Anns+ ( mkExportAnnT,+ mkLoc,+ mkParenT,+ setAnnsForT,+ swapEntryDPT,+ )+where++import Avail (AvailInfo (..))+import Data.Generics as SYB (Data)+import qualified Data.Map.Strict as Map (alter, fromList, insert, lookup, toList, union)+import Data.Maybe (fromMaybe)+import GHC+ ( AnnKeywordId (AnnCloseP, AnnDotdot, AnnOpenP, AnnVal),+ GhcPs,+ IE (..),+ IEWrappedName (..),+ LIEWrappedName,+ Name,+ RdrName,+ )+import GhcPlugins (GenLocated (L), Located, getOccFS, getOccString, mkVarUnqual)+import Language.Haskell.GHC.ExactPrint+ ( Annotation (annEntryDelta, annPriorComments, annsDP),+ TransformT,+ modifyAnnsT,+ uniqueSrcSpanT,+ )+import Language.Haskell.GHC.ExactPrint.Types+ ( DeltaPos (..),+ KeywordId (G),+ annNone,+ mkAnnKey,+ noExt,+ )+import Lexeme (isLexSym)++-- Generates the annotations for a name, wrapping () around symbollic names+mkLIEName :: Monad m => Name -> TransformT m (LIEWrappedName RdrName)+mkLIEName name = do+ let nameFS = getOccFS name+ let ann =+ if isLexSym nameFS -- infix type or data constructor / identifier+ then [(G AnnOpenP, DP (0, 0)), (G AnnVal, DP (0, 0)), (G AnnCloseP, DP (0, 0))]+ else [(G AnnVal, DP (0, 0))]+ lname <-+ mkLocWithAnns+ (mkVarUnqual nameFS)+ (DP (1, 2)) -- drop downn a line and indent 2 spaces+ ann+ mkLoc (IEName lname)++-- | Uses 'AvailInfo' about an exportable thing to generate the corresponding+-- piece of (annotated) AST+mkExportAnnT :: Monad m => AvailInfo -> TransformT m (Located (IE GhcPs))+-- Ordinary identifier+mkExportAnnT (Avail name) = do+ liename <- mkLIEName name+ mkLoc (IEVar noExt liename)++-- A type or class. Since we expect @name@ to be in scope, it should be the head+-- of @names@+mkExportAnnT (AvailTC name names fieldlabels) = do+ liename <- mkLIEName name++ -- Could export pieces explicitly, but this becomes ugly;+ -- operators need to be wrapped in (), etc, so just export things+ -- with pieces by wildcard+ let lienameWithWildcard =+ mkLocWithAnns+ (IEThingAll noExt liename)+ (DP (0, 0))+ [(G AnnOpenP, DP (0, 0)), (G AnnDotdot, DP (0, 0)), (G AnnCloseP, DP (0, 0))]++ case (names, fieldlabels) of+ -- This case implies that the type or class is not to be in scope+ -- which should not happen as we should only be processing exportable things+ -- Alternativey, could just: mkLoc (IEThingAbs noExt liename)+ ([], _) ->+ error $+ "smuggler: trying to export type class that is not to be in scope "+ ++ getOccString name+ -- A type class with no pieces+ ([_typeclass], []) -> mkLoc (IEThingAbs noExt liename)+ -- A type class with no pieces, but with field selectors. A record type?+ ([_typeclass], _fl) -> lienameWithWildcard+ -- A type class with pieces+ (typeorclass : _pieces, _fl) ->+ if name == typeorclass -- check AvailTC invariant+ then+ lienameWithWildcard+ else+ error $+ "smuggler: broken AvailTC invariant: "+ ++ getOccString name+ ++ "/="+ ++ getOccString typeorclass+++-------------------------------------------------------------------------------+-- Inspired by retrie++-- | Generates a unique location and wraps the given ast chunk with that location+-- Also adds a DP and an annotation at that location+mkLocWithAnns :: (Data e, Monad m) => e -> DeltaPos -> [(KeywordId, DeltaPos)] -> TransformT m (Located e)+mkLocWithAnns e dp anns = do+ le <- L <$> uniqueSrcSpanT <*> pure e+ setAnnsForT le dp anns++-- | `mkLoc` generates a unique location and wraps the given ast chunk with that location+-- Also adds an empty annotation at that location+mkLoc :: (Data e, Monad m) => e -> TransformT m (Located e)+mkLoc e = mkLocWithAnns e (DP (0, 0)) []++-- | Add an open and close paren annotation to a located thing+mkParenT ::+ (Data x, Monad m) =>+ (Located x -> x) ->+ Located x ->+ TransformT m (Located x)+mkParenT k e = do+ pe <- mkLoc (k e)+ _ <- setAnnsForT pe (DP (0, 0)) [(G GHC.AnnOpenP, DP (0, 1)), (G GHC.AnnCloseP, DP (0, 1))]+ swapEntryDPT e pe+ return pe++-- | Set the `ghc-exactprint` annotations for a 'Located' thing+setAnnsForT ::+ (Data e, Monad m) =>+ Located e ->+ DeltaPos ->+ [(KeywordId, DeltaPos)] ->+ TransformT m (Located e)+setAnnsForT e dp anns = modifyAnnsT (Map.alter f (mkAnnKey e)) >> return e+ where+ f Nothing = Just annNone {annEntryDelta = dp, annsDP = anns}+ f (Just a) =+ Just+ a+ { annEntryDelta = dp,+ annsDP =+ Map.toList $+ Map.union (Map.fromList anns) (Map.fromList (annsDP a))+ }++-- | Swap two 'Located' things' relative position tage ('DeltaPos')+swapEntryDPT ::+ (Data a, Data b, Monad m) => Located a -> Located b -> TransformT m ()+swapEntryDPT a b = modifyAnnsT $ \anns ->+ let akey = mkAnnKey a+ bkey = mkAnnKey b+ aann = fromMaybe annNone $ Map.lookup akey anns+ bann = fromMaybe annNone $ Map.lookup bkey anns+ in Map.insert+ akey+ aann+ { annEntryDelta = annEntryDelta bann,+ annPriorComments = annPriorComments bann+ }+ $ Map.insert+ bkey+ bann+ { annEntryDelta = annEntryDelta aann,+ annPriorComments = annPriorComments aann+ }+ anns
+ src/Smuggler2/Options.hs view
@@ -0,0 +1,50 @@+{- |+ Description: handling of command line options++ -}+module Smuggler2.Options+ ( Options (..),+ parseCommandLineOptions,+ ImportAction (..),+ ExportAction (..),+ )+where++import Data.Char (toLower)+import Data.List (foldl')+import Plugins (CommandLineOption)++-- | Ways of performing import processing+data ImportAction = NoImportProcessing | PreserveInstanceImports | MinimiseImports+ deriving (Eq, Show)++-- | Ways of performing emport processing+data ExportAction = NoExportProcessing | AddExplicitExports | ReplaceExports+ deriving (Eq, Show)++-- | Internal representation of the plugin's command line options+data Options = Options+ { importAction :: ImportAction,+ exportAction :: ExportAction,+ newExtension :: Maybe String+ }+ deriving (Show)++-- | The default is to retain instance-only imports (eg, Data.List () )+-- and add explict exports only if they are not already present+defaultOptions :: Options+defaultOptions = Options PreserveInstanceImports AddExplicitExports Nothing++-- | Simple command line option parser. Last occurrence wins.+parseCommandLineOptions :: [CommandLineOption] -> Options+parseCommandLineOptions = foldl' parseCommandLineOption defaultOptions+ where+ parseCommandLineOption :: Options -> CommandLineOption -> Options+ parseCommandLineOption opts clo = case toLower <$> clo of+ "noimportprocessing" -> opts {importAction = NoImportProcessing}+ "preserveinstanceimports" -> opts {importAction = PreserveInstanceImports}+ "minimiseimports" -> opts {importAction = MinimiseImports}+ "noexportprocessing" -> opts {exportAction = NoExportProcessing}+ "addexplicitexports" -> opts {exportAction = AddExplicitExports}+ "replaceexports" -> opts {exportAction = ReplaceExports}+ _ -> opts {newExtension = Just clo}
+ src/Smuggler2/Parser.hs view
@@ -0,0 +1,48 @@+{-|+ Description: provides a wrapper around the 'ghc-exactprint' parser+ -}+{-# LANGUAGE CPP #-}++module Smuggler2.Parser+ ( runParser,+ )+where++import Control.Monad.IO.Class (MonadIO (liftIO))+import DynFlags (DynFlags, GeneralFlag (Opt_KeepRawTokenStream), gopt_set)+import GHC (ParsedSource)+import Language.Haskell.GHC.ExactPrint (Anns)+import Language.Haskell.GHC.ExactPrint.Parsers (parseModuleFromStringInternal)+#if MIN_VERSION_GLASGOW_HASKELL(8,10,1,0)+import ErrUtils (fatalErrorMsg, printBagOfErrors)+import Outputable (text)+#else+import ErrUtils (fatalErrorMsg)+import Outputable (ppr, showSDoc, text)+#endif+import TcRnTypes (RnM)++-- | Wrapper around the 'ghc-exactprint' parser. Prints diagnostics for failed parses+-- (which should never happen). We need to use+-- 'Language.Haskell.GHC.ExactPrint.parseModuleFromStringInternal'+-- because 'Language.Haskell.GHC.ExactPrintparseModuleFromString'+-- doesn't pick up the correct 'DynFlags' in -- some cases.+runParser ::+ DynFlags -> FilePath -> String -> RnM (Either () (Anns, ParsedSource))+runParser dflags fileName fileContents = do+ -- Withoout the following, comments are stripped (see #10942)+ -- It would be more efficient, but less visible to apply this tweak at the+ -- outset, in the main plugin function, but keep it here for visibility+ let dflags' = dflags `gopt_set` Opt_KeepRawTokenStream++ case parseModuleFromStringInternal dflags' fileName fileContents of+ Left msg -> liftIO $ do+#if MIN_VERSION_GLASGOW_HASKELL(8,10,1,0)+ fatalErrorMsg dflags (text "smuggler parse failure:")+ printBagOfErrors dflags msg+#else+ fatalErrorMsg dflags (text $ "smuggler parse failure: " +++ showSDoc dflags (ppr $ fst msg) ++ ": " ++ snd msg)+#endif+ return $ Left ()+ Right x -> return $ Right x
+ src/Smuggler2/Plugin.hs view
@@ -0,0 +1,300 @@+{-|+ Description: the core of 'smuggler2'+ -}+{-# LANGUAGE LambdaCase #-}++module Smuggler2.Plugin+ ( plugin,+ )+where++import Avail (AvailInfo, Avails)+import Control.Monad (unless)+import Data.Maybe (fromMaybe, isNothing)+import Data.Version (showVersion)+import DynFlags (DynFlags (dumpDir), HasDynFlags (getDynFlags))+import ErrUtils (compilationProgressMsg, fatalErrorMsg)+import GHC+ ( GenLocated (L),+ GhcPs,+ HsModule (hsmodExports, hsmodImports),+ ImportDecl (ideclHiding, ideclImplicit),+ LIE,+ LImportDecl,+ Located,+ ModSummary (ms_hspp_buf, ms_hspp_file, ms_mod),+ Module (moduleName),+ ParsedSource,+ moduleNameString,+ unLoc,+ )+import GHC.IO.Encoding (setLocaleEncoding, utf8)+import IOEnv (MonadIO (liftIO), readMutVar)+import Language.Haskell.GHC.ExactPrint+ ( Anns,+ TransformT,+ addTrailingCommaT,+ exactPrint,+ graftT,+ runTransform,+ setEntryDPT,+ )+import Language.Haskell.GHC.ExactPrint.Types (DeltaPos (DP))+import Outputable (Outputable (ppr), neverQualify, printForUser, text, vcat)+import Paths_smuggler2 (version)+import Plugins+ ( CommandLineOption,+ Plugin (pluginRecompile, typeCheckResultAction),+ defaultPlugin,+ purePlugin,+ )+import RnNames (ImportDeclUsage, findImportUsage, getMinimalImports)+import Smuggler2.Anns (mkExportAnnT, mkLoc, mkParenT)+import Smuggler2.Options+ ( ExportAction (AddExplicitExports, NoExportProcessing, ReplaceExports),+ ImportAction (MinimiseImports, NoImportProcessing),+ Options (exportAction, importAction, newExtension),+ parseCommandLineOptions,+ )+import Smuggler2.Parser (runParser)+import StringBuffer (StringBuffer (StringBuffer), lexemeToString)+import System.Directory (removeFile)+import System.FilePath ((-<.>), (</>))+import System.IO (IOMode (WriteMode), withFile)+import TcRnExports (exports_from_avail)+import TcRnTypes+ ( RnM,+ TcGblEnv (tcg_exports, tcg_imports, tcg_mod, tcg_rdr_env, tcg_rn_exports, tcg_rn_imports, tcg_used_gres),+ TcM,+ )++-- | 'Plugin' interface to GHC+plugin :: Plugin+plugin =+ defaultPlugin+ { typeCheckResultAction = smugglerPlugin,+ pluginRecompile = purePlugin -- Don't force recompilation. [Is this the right approach?]+ }++-- | The plugin itself+smugglerPlugin :: [CommandLineOption] -> ModSummary -> TcGblEnv -> TcM TcGblEnv+smugglerPlugin clopts modSummary tcEnv+ -- short circuit, if nothing to do+ | (importAction options == NoImportProcessing)+ && (exportAction options == NoExportProcessing) =+ return tcEnv+ | otherwise = do+ -- Get the imports and their usage+ let imports = tcg_rn_imports tcEnv+ uses <- readMutVar $ tcg_used_gres tcEnv+ let usage = findImportUsage imports uses++ -- This ensures that the source file is not touched if there are no unused+ -- imports, or exports already exist and we are not replacing them+ let noUnusedImports = all (\(_decl, _used, unused) -> null unused) usage+ let hasExplicitExports = case tcg_rn_exports tcEnv of+ Nothing -> False -- There is not even a module header+ (Just []) -> False+ (Just _) -> True+ -- ... so short circuit if:+ -- - we are skipping import processing or there are no unused imports, and+ -- - we are skipping export processing or there are explict exports and we are not replacing them+ -- (This is not a complete check; ideally, that the new imp/exports are+ -- different from the existing ones, etc)+ if (importAction options == NoImportProcessing || noUnusedImports) &&+ ( exportAction options == NoExportProcessing+ || (hasExplicitExports && exportAction options /= ReplaceExports)+ )+ then return tcEnv+ else do+ dflags <- getDynFlags+ liftIO $ compilationProgressMsg dflags ("smuggler: " ++ showVersion version)++ -- Dump GHC's view of what the minimal imports are for the current+ -- module, so that they can be annotated when parsed back in+ -- This is needed because 'getMinimalImports` returns a list of import+ -- declarations that are `GhcRn` but `ghc-exactPrint` operates in+ -- `GhcPs`+ let minImpFilePath = mkMinimalImportsPath dflags (ms_mod modSummary)+ printMinimalImports' dflags minImpFilePath usage++ -- Run smuggling only for its side effects+ tcEnv <$ smuggling dflags minImpFilePath++ where++ -- Does all the work+ smuggling :: DynFlags -> FilePath -> RnM ()+ smuggling dflags minImpFilePath = do++ let modulePath = ms_hspp_file modSummary++ -- Read files as UTF-8 strings (GHC accepts only ASCII or UTF-8)+ liftIO $ setLocaleEncoding utf8++ -- Get the pre-processed module source code+ let modFileContents = case ms_hspp_buf modSummary of+ -- Not clear under what circumstances this could happen+ Nothing -> error $ "smuggler: missing source file: " ++ modulePath+ Just contents -> strBufToStr contents++ -- Parse the whole module+ runParser dflags modulePath modFileContents >>= \case+ Left () -> return () -- do nothing if file is invalid Haskell+ Right (annsHsMod, astHsMod) -> do+ -- Read the dumped file of minimal imports+ minImpFileContents <- liftIO $ readFile minImpFilePath++ -- Parse the minimal imports file -- gets the annnotations too+ runParser dflags minImpFilePath minImpFileContents >>= \case+ Left () ->+ liftIO $+ fatalErrorMsg dflags (text $ "smuggler: failed to parse minimal imports from " ++ minImpFilePath)+ Right (annsImpMod, L _ impMod) -> do++ -- The actual minimal imports themselves, as generated by GHC+ let minImports = hsmodImports impMod++ -- What is exported by the module+ exports <-+ if exportAction options == ReplaceExports+ then exportable+ else return $ tcg_exports tcEnv -- what is currently exported++ -- Bringing it all together: generate a new ast and annotations for it+ let (astHsMod', (annsHsMod', _locIndex), _log) =+ runTransform annsHsMod $+ replaceImports annsImpMod minImports astHsMod+ >>= addExplicitExports exports++ -- Print the result+ let newContent = exactPrint astHsMod' annsHsMod'+ liftIO $ case newExtension options of+ Nothing -> writeFile modulePath newContent+ Just ext -> writeFile (modulePath -<.> ext) newContent++ -- Clean up: delete the GHC-generated imports file+ liftIO $ removeFile minImpFilePath++ where++ -- Generates the things that would be exportabe if there were no+ -- explict export header, so suitable for replacing one+ exportable :: RnM [AvailInfo]+ exportable = do+ let rdr_env = tcg_rdr_env tcEnv+ let imports = tcg_imports tcEnv -- actually not needed for the Nothing case+ let this_mod = tcg_mod tcEnv+ exports <- exports_from_avail Nothing rdr_env imports this_mod+ return (snd exports)++ -- | Replace a target module's imports+ -- See <https://github.com/facebookincubator/retrie/blob/master/Retrie/CPP.hs>+ replaceImports ::+ Monad m =>+ -- | annotations for the replacement imports+ Anns ->+ -- | the replacement imports+ [LImportDecl GhcPs] ->+ -- | target module+ ParsedSource ->+ TransformT m ParsedSource+ replaceImports anns minImports t@(L l m) =+ case importAction options of+ NoImportProcessing -> return t+ _ -> do+ -- This does all the work+ -- retrie has a neat `insertImports' function that also+ -- deduplicates+ imps <- graftT anns minImports+ -- nudge down the imports list onto a new line+ unless (null imps) $ setEntryDPT (head imps) (DP (2, 0))+ return $ L l m {hsmodImports = imps}++ -- | Add explict exports to the target module+ addExplicitExports ::+ Monad m =>+ -- | The list of exports to be added+ Avails ->+ -- | target module+ ParsedSource ->+ TransformT m ParsedSource+ addExplicitExports exports t@(L astLoc hsMod) =+ case exportAction options of+ NoExportProcessing -> return t+ AddExplicitExports ->+ -- only add explicit exports if there are none+ -- seems to work even if there is no explict module declaration+ -- presumably because the annotations that we generate are just+ -- unused by exactPrint+ if isNothing currentExplicitExports then result else return t+ ReplaceExports -> result+ where+ currentExplicitExports :: Maybe (Located [LIE GhcPs])+ currentExplicitExports = hsmodExports hsMod++ -- This does all the export replacement work+ result :: Monad m => TransformT m ParsedSource+ result+ | null exports = return t -- there is nothing exportable+ | otherwise = do+ -- Generate the exports list+ exportsList <- mapM mkExportAnnT exports+ -- add commas in between and parens around+ mapM_ addTrailingCommaT (init exportsList)+ lExportsList <- mkLoc exportsList >>= mkParenT unLoc++ -- No need to do any graftTing here as we have been modifying the+ -- annotations in the current transformation+ return $ L astLoc hsMod {hsmodExports = Just lExportsList}++ -- | This version of the GHC function ignores implicit imports, as they+ -- cannot be parsed back in. (There is an extraneous (implicit))+ -- It also provides for leaving out instance-only imports (eg, Data.List() )+ printMinimalImports' :: DynFlags -> FilePath -> [ImportDeclUsage] -> RnM ()+ printMinimalImports' dflags filename imports_w_usage =+ do+ imports' <- getMinimalImports imports_w_usage+ liftIO $+ withFile+ filename+ WriteMode+ ( \h ->+ -- The neverQualify is important. We are printing Names+ -- but they are in the context of an 'import' decl, and+ -- we never qualify things inside there+ -- E.g. import Blag( f, b )+ -- not import Blag( Blag.f, Blag.g )!+ printForUser dflags h neverQualify (vcat (map ppr (filter (letThrough . unLoc) imports')))+ )+ where+ notImplicit :: ImportDecl pass -> Bool+ notImplicit = not . ideclImplicit++ notInstancesOnly :: ImportDecl pass -> Bool+ notInstancesOnly i = case ideclHiding i of+ Just (False, L _ []) -> False+ _ -> True++ keepInstanceOnlyImports :: Bool+ keepInstanceOnlyImports = importAction options /= MinimiseImports++ letThrough :: ImportDecl pass -> Bool+ letThrough i = notImplicit i && (keepInstanceOnlyImports || notInstancesOnly i)++ -- Construct the path into which GHC's version of minimal imports is dumped+ mkMinimalImportsPath :: DynFlags -> Module -> FilePath+ mkMinimalImportsPath dflags this_mod+ | Just d <- dumpDir dflags = d </> basefn+ | otherwise = basefn+ where+ basefn =+ "smuggler-" ++ moduleNameString (moduleName this_mod) ++ "."+ ++ fromMaybe "smuggler" (newExtension options)+ ++ ".imports"++ options :: Options+ options = parseCommandLineOptions clopts++ strBufToStr :: StringBuffer -> String+ strBufToStr sb@(StringBuffer _ len _) = lexemeToString sb len
+ test/Test.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Data.Maybe (fromMaybe)+import GHC.Paths (ghc)+import Smuggler2.Options (ExportAction (..), ImportAction (..), Options (..))+import System.Environment (lookupEnv)+import System.FilePath ((-<.>), (</>), takeBaseName)+import System.Process.Typed+ ( ProcessConfig,+ proc,+ runProcess_,+ setEnvInherit,+ setWorkingDirInherit,+ )+import Test.Tasty (TestTree, defaultMain, testGroup)+import Test.Tasty.Golden (findByExtension, goldenVsFileDiff, writeBinaryFile)++-- | Where the tests are, relative to the project level cabal file+testDir :: FilePath+testDir = "test" </> "tests"++-- | Combinations of import and export action options to be tested+optionsList :: [Options]+optionsList =+ [ mkOptions PreserveInstanceImports NoExportProcessing,+ mkOptions MinimiseImports ReplaceExports,+ mkOptions MinimiseImports NoExportProcessing,+ mkOptions NoImportProcessing AddExplicitExports,+ mkOptions NoImportProcessing NoExportProcessing,+ mkOptions NoImportProcessing ReplaceExports+ ]+ where+ mkOptions :: ImportAction -> ExportAction -> Options+ mkOptions ia ea = Options ia ea (Just $ mkExt ia ea)++-- | Make an extention for an output file+mkExt :: ImportAction -> ExportAction -> String+mkExt ia ea = show ia ++ show ea -- ++ "-" ++ takeFileName ghc++-- | Generate test for a list of 'Options' each of which specify what action to+-- take on imports and exports+testOptions :: [Options] -> IO TestTree+testOptions opts =+ testGroup "All" <$> sequenceA (goldenTests <$> opts)++-- | Generate tests for a set of 'Options' (that specifies what to do to+-- imports and exports)+goldenTests :: Options -> IO TestTree+goldenTests opts = do+ testFiles <- findByExtension [".hs"] testDir+ return $+ testGroup+ testName+ [ goldenVsFileDiff+ (takeBaseName testFile) -- test name+ (\ref new -> ["git", "diff", "--no-index", ref, new]) -- how to display diffs+ (testFile -<.> testName ++ "-golden") -- golden file+ outputFilename+ ( do+ -- Write a default output file for those tests where smuggler+ -- (deliberately) does not generate a new one+ writeBinaryFile outputFilename "Source file was not touched\r\n"+ compile testFile opts+ )+ | testFile <- testFiles,+ let outputFilename = testFile -<.> testName+ ]+ where+ testName = fromMaybe "NoNewExtension" (newExtension opts)++-- | Just run all the tests+main :: IO ()+main = defaultMain =<< testOptions optionsList++-- | Use `cabal exec` to run the compilation, so that the smuggler plugin is+-- picked up from the local database. GHC alone would use the global one.+compile :: FilePath -> Options -> IO ()+compile testcase opts = do+ cabalPath <- lookupEnv "CABAL" -- find, eg, @/opt/ghc/bin/cabal@ or @cabal -vnormal+nowrap@+ let cabalCmd = words $ fromMaybe "cabal" cabalPath -- default to @cabal@ if @CABAL@ is not set+ let cabalConfig =+ setWorkingDirInherit . setEnvInherit $+ proc (head cabalCmd) (tail cabalCmd ++ cabalArgs) :: ProcessConfig () () ()+ runProcess_ cabalConfig+ where+ cabalArgs :: [String]+ cabalArgs =+ -- - not sure why it is necessary to mention the smuggler2 package explicitly,+ -- but it appears to be hidden otherwise.+ -- - This puts the .imports files that smuggler generates somewhere they+ -- can easily be found+ [ "--with-compiler=" ++ ghc,+ "exec",+ ghc,+ "--",+ "-package smuggler2",+ "-v0",+ "-dumpdir=" ++ testDir,+ "-fno-code",+ "-fplugin=Smuggler2.Plugin"+ ]+ ++ map+ ("-fplugin-opt=Smuggler2.Plugin:" ++)+ ( let ia = importAction opts+ ea = exportAction opts+ p = [show ia, show ea]+ in case newExtension opts of+ Nothing -> mkExt ia ea : p -- provide a default extension+ Just e -> e : p+ )+ ++ [testcase]
+ test/tests/Bare.hs view
@@ -0,0 +1,4 @@++import Data.List++main = undefined
+ test/tests/BareHello.hs view
@@ -0,0 +1,3 @@+module BareHello where++main = putStrLn "Hello, World"
+ test/tests/BoolBoolUnused.hs view
@@ -0,0 +1,3 @@+module Test.BoolBoolUnused where++import Data.Bool (bool)
+ test/tests/BoolBoolUsed.hs view
@@ -0,0 +1,5 @@+module Test.BoolBoolUsed where++import Data.Bool (bool)++func = bool
+ test/tests/BoolPartlyUsedFirst.hs view
@@ -0,0 +1,5 @@+module Test.BoolPartlyUsedFirst where++import Data.Bool (bool, not)++foo = bool
+ test/tests/BoolPartlyUsedLast.hs view
@@ -0,0 +1,5 @@+module Test.BoolPartlyUsedLast where++import Data.Bool (bool, not)++foo = not
+ test/tests/Class.hs view
@@ -0,0 +1,42 @@+module Class where+++data Record a = Record { aa :: a, b :: String }++data Impossible++ = Impossible String Integer+ -- ^ We reached a program point which should be unreachable.++ | Unreachable String Integer+ -- ^ @Impossible@ with a different error message.+ -- Used when we reach a program point which can in principle+ -- be reached, but not for a certain run.++ | ImpMissingDefinitions [String] String+ -- ^ We reached a program point without all the required+ -- primitives or BUILTIN to proceed forward.+ -- @ImpMissingDefinitions neededDefs forThis@+++class CatchImpossible m where++ -- | Catch any 'Impossible' exception.+ catchImpossible :: m a -> (Impossible -> m a) -> m a+ catchImpossible = catchImpossibleJust Just++ -- | Catch only 'Impossible' exceptions selected by the filter.+ catchImpossibleJust :: (Impossible -> Maybe b) -> m a -> (b -> m a) -> m a+ catchImpossibleJust = flip . handleImpossibleJust++ -- | Version of 'catchImpossible' with argument order suiting short handlers.+ handleImpossible :: (Impossible -> m a) -> m a -> m a+ handleImpossible = flip catchImpossible++ -- | Version of 'catchImpossibleJust' with argument order suiting short handlers.+ handleImpossibleJust :: (Impossible -> Maybe b) -> (b -> m a) -> m a -> m a+ handleImpossibleJust = flip . catchImpossibleJust++ {-# MINIMAL catchImpossibleJust | handleImpossibleJust #-}++
+ test/tests/ConstructorsUnused.hs view
@@ -0,0 +1,5 @@+module Test.ConstructorsUnused where++import Data.Bool (Bool (True, False), not)++foo = not
+ test/tests/Dummy.hs view
@@ -0,0 +1,4 @@+module Test.Dummy where++main :: IO ()+main = pure ()
+ test/tests/Duplicated.hs view
@@ -0,0 +1,8 @@+module Duplicated where++import Data.Char (isDigit, isSpace)+import Data.Char (isPrint, isSpace)+import Data.Char (isLetter, isSpace)+++test x = isDigit x || isLetter x
+ test/tests/ExportExplicit.hs view
@@ -0,0 +1,10 @@+module Test.ExportExplicit (isExported, anotherExport) where++isExported = (+1)++anotherExport = True++andAnother = 27++main :: IO ()+main = pure ()
+ test/tests/ExportImplicit.hs view
@@ -0,0 +1,10 @@+module Test.ExportImplicit where++import Data.Char++isExported = (+1)++alsoExported = map toLower "AbC"++main :: IO ()+main = print $ isExported 1
+ test/tests/ImplicitImportUnused.hs view
@@ -0,0 +1,9 @@+module Test.ImplicitImportUnused where++import Data.List+import Data.Bool (not, (&&))+import Data.Maybe (fromMaybe, isJust, isNothing)++foo = isJust++bar = 27
+ test/tests/ImplicitImportUsed.hs view
@@ -0,0 +1,5 @@+module Test.ImplicitImportUsed where++import Data.List++foo = intercalate ","
+ test/tests/ImportEmpty.hs view
@@ -0,0 +1,3 @@+module Test.ImportEmpty where++import Data.Bool ()
+ test/tests/MoreExports.hs view
@@ -0,0 +1,20 @@+module MoreExports where++--Another comment+--++newtype N a = Int a++data D a = A a | B | C a++data R = R { a :: Int, b :: Bool }++data H = I | J | K deriving Show+++foo = (+1)++-- A further Comment++main :: IO ()+main = print J
+ test/tests/MultilineUnused.hs view
@@ -0,0 +1,10 @@+module Test.MultilineUnused where++import Data.Bool (bool)+import Data.List (break, cycle, drop, dropWhile, filter, genericDrop, genericLength,+ genericReplicate, genericSplitAt, genericTake, group, inits, intercalate,+ intersperse, isPrefixOf, iterate, map, permutations, repeat, replicate, reverse,+ scanl, scanr, sort, sortBy, sortOn, splitAt, subsequences, tails, take, takeWhile,+ transpose, unfoldr, unzip, unzip3, zip, zip3, zipWith, (++))++foo = bool
+ test/tests/Operator.hs view
@@ -0,0 +1,8 @@+module Operator where++infixl 5 $+$++($+$) :: [a] -> [a] -> [a]+[] $+$ d = d+d $+$ [] = d+d $+$ d' = d ++ d'
+ test/tests/QualUnqual.hs view
@@ -0,0 +1,6 @@+module QualUnqual where++import qualified Data.Char as C (isDigit, isLetter)+import Data.Char (isDigit, isLetter)++test x = C.isDigit x || isDigit x
+ test/tests/TypeConstructorUnused.hs view
@@ -0,0 +1,5 @@+module Test.TypeConstructorUnused where++import Data.Bool (Bool (False, True))++true = True
+ test/tests/TypeConstructorUnusedComma.hs view
@@ -0,0 +1,5 @@+module Test.TypeConstructorUnusedComma where++import Data.Bool (Bool (False, True))++false = False
+ test/tests/TypeConstructorUsed.hs view
@@ -0,0 +1,5 @@+module Test.TypeConstructorUsed where++import Data.Bool (Bool (True))++true = True
+ test/tests/TypeUnused.hs view
@@ -0,0 +1,5 @@+module Test.TypeUnused where++import Data.Bool (Bool, not)++foo = not
+ test/tests/TypeUsed.hs view
@@ -0,0 +1,6 @@+module Test.TypeUsed where++import Data.Bool (Bool, not)++foo :: Bool+foo = undefined
+ test/tests/WildcardUnused.hs view
@@ -0,0 +1,5 @@+module Test.WildcardUnused where++import Data.Bool (Bool (..), not)++foo = not
+ test/tests/WildcardUsed.hs view
@@ -0,0 +1,5 @@+module Test.WildcardUsed where++import Data.Bool (Bool (..))++foo = False
+ weeder.dhall view
@@ -0,0 +1,1 @@+{ roots = [ "^Smuggler2.Plugin.plugin$", "^Main.main$", "^Paths_smuggler2" ], type-class-roots = True }