diff --git a/.gitignore b/.gitignore index 61ecc03..f280993 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,14 @@ package.cache cabal.sandbox.config # Mac OS generates # .DS_Store +*.o +*.dyn_o +*.hi +*.dyn_hi # Where do these files come from? They're not readable. # For instance, .#Help.page # .#* cabal-dev +/TAGS +/tags diff --git a/.travis.yml b/.travis.yml index f21baf1..c134ceb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,36 @@ ghc: - 7.6 - 7.8 +sudo: false + +addons: + apt: + packages: + - zlib1g-dev + +cache: + apt: true + directories: + - ~/.cabal + - ~/.ghc + +before_cache: + - rm -f $HOME/.cabal/logs $HOME/.cabal/packages/*/build-reports.log + install: - cabal update - - cabal install happy --constraint 'transformers <= 0.3.0.0' +# - ( $CABAL122 && cabal install cabal-install --constraint "Cabal >= 1.22" && ghc-pkg unregister Cabal ) || true + - echo $PATH + - which cabal + - if [ -n "$(cabal --version | grep 'Cabal library' | awk '{ print $3 }' | tail -n1 | sed -n '/^1.18/p')" ]; then cabal install cabal-install --constraint "Cabal == 1.18.* && > 1.18.0"; fi + - cabal install happy - happy --version +# - ls -lR ~/.ghc +# - ls -lR ~/.cabal - cabal install -j --only-dependencies --enable-tests + - git clone --depth=1 https://github.com/DanielG/cabal-helper.git + - cabal install cabal-helper/ + script: - touch ChangeLog # Create ChangeLog if we're not on the release branch @@ -21,6 +46,7 @@ script: - if [ -n "$(ghc --version | awk '{ print $8 }' | sed -n '/^7.8/p')" ]; then export WERROR="--ghc-option=-Werror"; fi - cabal configure --enable-tests $WERROR - cabal build + - export ghc_mod_datadir=$PWD - cabal test matrix: diff --git a/COPYING.AGPL3 b/COPYING.AGPL3 new file mode 100644 index 0000000..dba13ed --- /dev/null +++ b/COPYING.AGPL3 @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/COPYING.BSD3 b/COPYING.BSD3 new file mode 100644 index 0000000..5422193 --- /dev/null +++ b/COPYING.BSD3 @@ -0,0 +1,29 @@ +Copyright (c) 2009, IIJ Innovation Institute Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE b/LICENSE index 5422193..c646aeb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,29 +1,6 @@ -Copyright (c) 2009, IIJ Innovation Institute Inc. -All rights reserved. +ghc-mod was originally licensed under the BSD3 but the primary license has been +changed to the AGPL3, files originally contributed under the BSD3 license remain +under this license and can generally be identified by the lack of a GPL header. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - * Neither the name of the copyright holders nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. +See the files COPYING.BSD3 and COPYING.AGPL3 in the source distribution for +copies of the two licenses. diff --git a/Language/Haskell/GhcMod.hs b/Language/Haskell/GhcMod.hs index b356efa..d1eecd8 100644 --- a/Language/Haskell/GhcMod.hs +++ b/Language/Haskell/GhcMod.hs @@ -3,15 +3,22 @@ module Language.Haskell.GhcMod ( -- * Cradle Cradle(..) + , ProjectType(..) , findCradle -- * Options , Options(..) , LineSeparator(..) , OutputStyle(..) , defaultOptions + -- * Logging + , GmLogLevel + , increaseLogLevel + , decreaseLogLevel + , gmSetLogLevel + , gmLog -- * Types , ModuleString - , Expression + , Expression(..) , GhcPkgDb , Symbol , SymbolDb @@ -22,12 +29,14 @@ module Language.Haskell.GhcMod ( -- * Monad utilities , runGhcModT , withOptions + , dropSession -- * 'GhcMod' utilities , boot , browse , check , checkSyntax , debugInfo + , componentInfo , expandTemplate , info , lint @@ -47,6 +56,13 @@ module Language.Haskell.GhcMod ( -- * SymbolDb , loadSymbolDb , isOutdated + -- * Output + , gmPutStr + , gmErrStr + , gmPutStrLn + , gmErrStrLn + , gmUnsafePutStrLn + , gmUnsafeErrStrLn ) where import Language.Haskell.GhcMod.Boot @@ -61,7 +77,10 @@ import Language.Haskell.GhcMod.Flag import Language.Haskell.GhcMod.Info import Language.Haskell.GhcMod.Lang import Language.Haskell.GhcMod.Lint -import Language.Haskell.GhcMod.Monad +import Language.Haskell.GhcMod.Logging import Language.Haskell.GhcMod.Modules +import Language.Haskell.GhcMod.Monad import Language.Haskell.GhcMod.PkgDoc import Language.Haskell.GhcMod.Types +import Language.Haskell.GhcMod.Target +import Language.Haskell.GhcMod.Output diff --git a/Language/Haskell/GhcMod/Boot.hs b/Language/Haskell/GhcMod/Boot.hs index 7e261d5..c0abae5 100644 --- a/Language/Haskell/GhcMod/Boot.hs +++ b/Language/Haskell/GhcMod/Boot.hs @@ -1,6 +1,7 @@ module Language.Haskell.GhcMod.Boot where import Control.Applicative +import Prelude import Language.Haskell.GhcMod.Browse import Language.Haskell.GhcMod.Flag import Language.Haskell.GhcMod.Lang @@ -9,8 +10,9 @@ import Language.Haskell.GhcMod.Modules -- | Printing necessary information for front-end booting. boot :: IOish m => GhcModT m String -boot = concat <$> sequence [modules, languages, flags, - concat <$> mapM browse preBrowsedModules] +boot = concat <$> sequence ms + where + ms = [modules, languages, flags, concat <$> mapM browse preBrowsedModules] preBrowsedModules :: [String] preBrowsedModules = [ diff --git a/Language/Haskell/GhcMod/Browse.hs b/Language/Haskell/GhcMod/Browse.hs index b5fc4aa..6093c9e 100644 --- a/Language/Haskell/GhcMod/Browse.hs +++ b/Language/Haskell/GhcMod/Browse.hs @@ -2,54 +2,57 @@ module Language.Haskell.GhcMod.Browse ( browse ) where -import Control.Applicative ((<$>)) +import Control.Applicative import Control.Exception (SomeException(..)) -import Data.Char (isAlpha) -import Data.List (sort) -import Data.Maybe (catMaybes) -import Exception (ghandle) -import FastString (mkFastString) -import GHC (GhcException(CmdLineError), ModuleInfo, Name, TyThing, DynFlags, Type, TyCon) +import Data.Char +import Data.List +import Data.Maybe +import FastString +import GHC import qualified GHC as G import Language.Haskell.GhcMod.Convert import Language.Haskell.GhcMod.Doc (showPage, styleUnqualified) -import Language.Haskell.GhcMod.Gap -import Language.Haskell.GhcMod.Monad (GhcModT, options) -import Language.Haskell.GhcMod.Target (setTargetFiles) +import Language.Haskell.GhcMod.Gap as Gap +import Language.Haskell.GhcMod.Monad import Language.Haskell.GhcMod.Types import Name (getOccString) -import Outputable (ppr, Outputable) +import Outputable import TyCon (isAlgTyCon) import Type (dropForAlls, splitFunTy_maybe, mkFunTy, isPredTy) +import Exception (ExceptionMonad, ghandle) +import Prelude ---------------------------------------------------------------- -- | Getting functions, classes, etc from a module. -- If 'detailed' is 'True', their types are also obtained. -- If 'operators' is 'True', operators are also returned. -browse :: IOish m - => ModuleString -- ^ A module name. (e.g. \"Data.List\") +browse :: forall m. IOish m + => String -- ^ A module name. (e.g. \"Data.List\", "base:Prelude") -> GhcModT m String -browse pkgmdl = convert' . sort =<< (listExports =<< getModule) +browse pkgmdl = do + convert' . sort =<< go where - (mpkg,mdl) = splitPkgMdl pkgmdl + -- TODO: Add API to Gm.Target to check if module is home module without + -- bringing up a GHC session as well then this can be made a lot cleaner + go = ghandle (\(SomeException _) -> return []) $ do + goPkgModule `G.gcatch` (\(SomeException _) -> goHomeModule) + + goPkgModule = do + opt <- options + runGmPkgGhc $ + processExports opt =<< tryModuleInfo =<< G.findModule mdlname mpkgid + + goHomeModule = runGmlT [Right mdlname] $ do + opt <- options + processExports opt =<< tryModuleInfo =<< G.findModule mdlname Nothing + + tryModuleInfo m = fromJust <$> G.getModuleInfo m + + (mpkg, mdl) = splitPkgMdl pkgmdl mdlname = G.mkModuleName mdl mpkgid = mkFastString <$> mpkg - listExports Nothing = return [] - listExports (Just mdinfo) = processExports mdinfo - -- findModule works only for package modules, moreover, - -- you cannot load a package module. On the other hand, - -- to browse a local module you need to load it first. - -- If CmdLineError is signalled, we assume the user - -- tried browsing a local module. - getModule = browsePackageModule `G.gcatch` fallback `G.gcatch` handler - browsePackageModule = G.findModule mdlname mpkgid >>= G.getModuleInfo - browseLocalModule = ghandle handler $ do - setTargetFiles [mdl] - G.findModule mdlname Nothing >>= G.getModuleInfo - fallback (CmdLineError _) = browseLocalModule - fallback _ = return Nothing - handler (SomeException _) = return Nothing + -- | -- -- >>> splitPkgMdl "base:Prelude" @@ -57,9 +60,10 @@ browse pkgmdl = convert' . sort =<< (listExports =<< getModule) -- >>> splitPkgMdl "Prelude" -- (Nothing,"Prelude") splitPkgMdl :: String -> (Maybe String,String) -splitPkgMdl pkgmdl = case break (==':') pkgmdl of - (mdl,"") -> (Nothing,mdl) - (pkg,_:mdl) -> (Just pkg,mdl) +splitPkgMdl pkgmdl = + case break (==':') pkgmdl of + (mdl, "") -> (Nothing, mdl) + (pkg, _:mdl) -> (Just pkg, mdl) -- Haskell 2010: -- small -> ascSmall | uniSmall | _ @@ -71,22 +75,23 @@ isNotOp :: String -> Bool isNotOp (h:_) = isAlpha h || (h == '_') isNotOp _ = error "isNotOp" -processExports :: IOish m => ModuleInfo -> GhcModT m [String] -processExports minfo = do - opt <- options +processExports :: (G.GhcMonad m, MonadIO m, ExceptionMonad m) + => Options -> ModuleInfo -> m [String] +processExports opt minfo = do let removeOps | operators opt = id | otherwise = filter (isNotOp . getOccString) mapM (showExport opt minfo) $ removeOps $ G.modInfoExports minfo -showExport :: IOish m => Options -> ModuleInfo -> Name -> GhcModT m String +showExport :: forall m. (G.GhcMonad m, MonadIO m, ExceptionMonad m) + => Options -> ModuleInfo -> Name -> m String showExport opt minfo e = do mtype' <- mtype return $ concat $ catMaybes [mqualified, Just $ formatOp $ getOccString e, mtype'] where mqualified = (G.moduleNameString (G.moduleName $ G.nameModule e) ++ ".") `justIf` qualified opt - mtype :: IOish m => GhcModT m (Maybe String) + mtype :: m (Maybe String) mtype | detailed opt = do tyInfo <- G.modInfoLookupName minfo e @@ -101,8 +106,9 @@ showExport opt minfo e = do | null nm = error "formatOp" | isNotOp nm = nm | otherwise = "(" ++ nm ++ ")" - inOtherModule :: IOish m => Name -> GhcModT m (Maybe TyThing) - inOtherModule nm = G.getModuleInfo (G.nameModule nm) >> G.lookupGlobalName nm + inOtherModule :: Name -> m (Maybe TyThing) + inOtherModule nm = do + G.getModuleInfo (G.nameModule nm) >> G.lookupGlobalName nm justIf :: a -> Bool -> Maybe a justIf x True = Just x justIf _ False = Nothing @@ -127,7 +133,7 @@ tyType typ && not (G.isClassTyCon typ) = Just "data" | G.isNewTyCon typ = Just "newtype" | G.isClassTyCon typ = Just "class" - | G.isSynTyCon typ = Just "type" + | Gap.isSynTyCon typ = Just "type" | otherwise = Nothing removeForAlls :: Type -> Type diff --git a/Language/Haskell/GhcMod/Cabal18.hs b/Language/Haskell/GhcMod/Cabal18.hs deleted file mode 100644 index 94451a7..0000000 --- a/Language/Haskell/GhcMod/Cabal18.hs +++ /dev/null @@ -1,58 +0,0 @@ --- Copyright : Isaac Jones 2003-2004 -{- All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of Isaac Jones nor the names of other - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -} - --- | ComponentLocalBuildInfo for Cabal >= 1.18 -module Language.Haskell.GhcMod.Cabal18 ( - ComponentLocalBuildInfo - , componentPackageDeps - , componentLibraries - ) where - -import Distribution.Package (InstalledPackageId, PackageId) - -data LibraryName = LibraryName String - deriving (Read, Show) - -data ComponentLocalBuildInfo - = LibComponentLocalBuildInfo { - componentPackageDeps :: [(InstalledPackageId, PackageId)], - componentLibraries :: [LibraryName] - } - | ExeComponentLocalBuildInfo { - componentPackageDeps :: [(InstalledPackageId, PackageId)] - } - | TestComponentLocalBuildInfo { - componentPackageDeps :: [(InstalledPackageId, PackageId)] - } - | BenchComponentLocalBuildInfo { - componentPackageDeps :: [(InstalledPackageId, PackageId)] - } - deriving (Read, Show) diff --git a/Language/Haskell/GhcMod/Cabal21.hs b/Language/Haskell/GhcMod/Cabal21.hs deleted file mode 100644 index 164e5d6..0000000 --- a/Language/Haskell/GhcMod/Cabal21.hs +++ /dev/null @@ -1,73 +0,0 @@ --- Copyright : Isaac Jones 2003-2004 -{- All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of Isaac Jones nor the names of other - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -} - --- | ComponentLocalBuildInfo for Cabal >= 1.21 -module Language.Haskell.GhcMod.Cabal21 ( - ComponentLocalBuildInfo - , PackageIdentifier(..) - , PackageName(..) - , componentPackageDeps - , componentLibraries - ) where - -import Distribution.Package (InstalledPackageId) -import Data.Version (Version) - -data LibraryName = LibraryName String - deriving (Read, Show) - -newtype PackageName = PackageName { unPackageName :: String } - deriving (Read, Show) - -data PackageIdentifier - = PackageIdentifier { - pkgName :: PackageName, - pkgVersion :: Version - } - deriving (Read, Show) - -type PackageId = PackageIdentifier - -data ComponentLocalBuildInfo - = LibComponentLocalBuildInfo { - componentPackageDeps :: [(InstalledPackageId, PackageId)], - componentLibraries :: [LibraryName] - } - | ExeComponentLocalBuildInfo { - componentPackageDeps :: [(InstalledPackageId, PackageId)] - } - | TestComponentLocalBuildInfo { - componentPackageDeps :: [(InstalledPackageId, PackageId)] - } - | BenchComponentLocalBuildInfo { - componentPackageDeps :: [(InstalledPackageId, PackageId)] - } - deriving (Read, Show) diff --git a/Language/Haskell/GhcMod/CabalApi.hs b/Language/Haskell/GhcMod/CabalApi.hs deleted file mode 100644 index fc56adb..0000000 --- a/Language/Haskell/GhcMod/CabalApi.hs +++ /dev/null @@ -1,193 +0,0 @@ -{-# LANGUAGE OverloadedStrings, CPP #-} - -module Language.Haskell.GhcMod.CabalApi ( - getCompilerOptions - , parseCabalFile - , cabalAllBuildInfo - , cabalDependPackages - , cabalSourceDirs - , cabalAllTargets - , cabalConfigDependencies - ) where - -import Language.Haskell.GhcMod.CabalConfig -import Language.Haskell.GhcMod.Error -import Language.Haskell.GhcMod.Gap (benchmarkBuildInfo, benchmarkTargets, - toModuleString) -import Language.Haskell.GhcMod.GhcPkg -import Language.Haskell.GhcMod.Types - -import MonadUtils (liftIO) -import Control.Applicative ((<$>)) -import qualified Control.Exception as E -import Control.Monad (filterM) -import Data.Maybe (maybeToList) -import Data.Set (fromList, toList) -import Distribution.Package (Dependency(Dependency) - , PackageName(PackageName)) -import qualified Distribution.Package as C -import Distribution.PackageDescription (PackageDescription, BuildInfo, TestSuite, TestSuiteInterface(..), Executable) -import qualified Distribution.PackageDescription as P -import Distribution.PackageDescription.Configuration (finalizePackageDescription) -import Distribution.PackageDescription.Parse (readPackageDescription) -import Distribution.Simple.Compiler (CompilerId(..), CompilerFlavor(..)) -import Distribution.Simple.Program as C (ghcProgram) -import Distribution.Simple.Program.Types (programName, programFindVersion) -import Distribution.System (buildPlatform) -import Distribution.Text (display) -import Distribution.Verbosity (silent) -import Distribution.Version (Version) -import System.Directory (doesFileExist) -import System.FilePath (()) ----------------------------------------------------------------- - --- | Getting necessary 'CompilerOptions' from three information sources. -getCompilerOptions :: (IOish m, MonadError GhcModError m) - => [GHCOption] - -> Cradle - -> PackageDescription - -> m CompilerOptions -getCompilerOptions ghcopts cradle pkgDesc = do - gopts <- liftIO $ getGHCOptions ghcopts cradle rdir $ head buildInfos - depPkgs <- cabalConfigDependencies cradle (C.packageId pkgDesc) - return $ CompilerOptions gopts idirs depPkgs - where - wdir = cradleCurrentDir cradle - rdir = cradleRootDir cradle - buildInfos = cabalAllBuildInfo pkgDesc - idirs = includeDirectories rdir wdir $ cabalSourceDirs buildInfos - ----------------------------------------------------------------- --- Include directories for modules - -cabalBuildDirs :: [FilePath] -cabalBuildDirs = ["dist/build", "dist/build/autogen"] - -includeDirectories :: FilePath -> FilePath -> [FilePath] -> [FilePath] -includeDirectories cdir wdir dirs = uniqueAndSort (extdirs ++ [cdir,wdir]) - where - extdirs = map expand $ dirs ++ cabalBuildDirs - expand "." = cdir - expand subdir = cdir subdir - ----------------------------------------------------------------- - --- | Parse a cabal file and return a 'PackageDescription'. -parseCabalFile :: (IOish m, MonadError GhcModError m) - => Cradle - -> FilePath - -> m PackageDescription -parseCabalFile cradle file = do - cid <- liftIO getGHCId - epgd <- liftIO $ readPackageDescription silent file - flags <- cabalConfigFlags cradle - case toPkgDesc cid flags epgd of - Left deps -> fail $ show deps ++ " are not installed" - Right (pd,_) -> if nullPkg pd - then fail $ file ++ " is broken" - else return pd - where - toPkgDesc cid flags = - finalizePackageDescription flags (const True) buildPlatform cid [] - nullPkg pd = name == "" - where - PackageName name = C.pkgName (P.package pd) - ----------------------------------------------------------------- - -getGHCOptions :: [GHCOption] -> Cradle -> FilePath -> BuildInfo -> IO [GHCOption] -getGHCOptions ghcopts cradle rdir binfo = do - cabalCpp <- cabalCppOptions rdir - let cpps = map ("-optP" ++) $ P.cppOptions binfo ++ cabalCpp - return $ ghcopts ++ pkgDb ++ exts ++ [lang] ++ libs ++ libDirs ++ cpps - where - pkgDb = ghcDbStackOpts $ cradlePkgDbStack cradle - lang = maybe "-XHaskell98" (("-X" ++) . display) $ P.defaultLanguage binfo - libDirs = map ("-L" ++) $ P.extraLibDirs binfo - exts = map (("-X" ++) . display) $ P.usedExtensions binfo - libs = map ("-l" ++) $ P.extraLibs binfo - -cabalCppOptions :: FilePath -> IO [String] -cabalCppOptions dir = do - exist <- doesFileExist cabalMacro - return $ if exist then - ["-include", cabalMacro] - else - [] - where - cabalMacro = dir "dist/build/autogen/cabal_macros.h" - ----------------------------------------------------------------- - --- | Extracting all 'BuildInfo' for libraries, executables, and tests. -cabalAllBuildInfo :: PackageDescription -> [BuildInfo] -cabalAllBuildInfo pd = libBI ++ execBI ++ testBI ++ benchBI - where - libBI = map P.libBuildInfo $ maybeToList $ P.library pd - execBI = map P.buildInfo $ P.executables pd - testBI = map P.testBuildInfo $ P.testSuites pd - benchBI = benchmarkBuildInfo pd - ----------------------------------------------------------------- - --- | Extracting package names of dependency. -cabalDependPackages :: [BuildInfo] -> [PackageBaseName] -cabalDependPackages bis = uniqueAndSort pkgs - where - pkgs = map getDependencyPackageName $ concatMap P.targetBuildDepends bis - getDependencyPackageName (Dependency (PackageName nm) _) = nm - ----------------------------------------------------------------- - --- | Extracting include directories for modules. -cabalSourceDirs :: [BuildInfo] -> [IncludeDir] -cabalSourceDirs bis = uniqueAndSort $ concatMap P.hsSourceDirs bis - ----------------------------------------------------------------- - -uniqueAndSort :: [String] -> [String] -uniqueAndSort = toList . fromList - ----------------------------------------------------------------- - -getGHCId :: IO CompilerId -getGHCId = CompilerId GHC <$> getGHC - -getGHC :: IO Version -getGHC = do - mv <- programFindVersion C.ghcProgram silent (programName C.ghcProgram) - case mv of - -- TODO: MonadError it up - Nothing -> E.throwIO $ userError "ghc not found" - Just v -> return v - ----------------------------------------------------------------- - --- | Extracting all 'Module' 'FilePath's for libraries, executables, --- tests and benchmarks. -cabalAllTargets :: PackageDescription -> IO ([String],[String],[String],[String]) -cabalAllTargets pd = do - exeTargets <- mapM getExecutableTarget $ P.executables pd - testTargets <- mapM getTestTarget $ P.testSuites pd - return (libTargets,concat exeTargets,concat testTargets,benchTargets) - where - lib = case P.library pd of - Nothing -> [] - Just l -> P.libModules l - - libTargets = map toModuleString lib - benchTargets = benchmarkTargets pd - - getTestTarget :: TestSuite -> IO [String] - getTestTarget ts = - case P.testInterface ts of - (TestSuiteExeV10 _ filePath) -> do - let maybeTests = [p e | p <- P.hsSourceDirs $ P.testBuildInfo ts, e <- [filePath]] - liftIO $ filterM doesFileExist maybeTests - (TestSuiteLibV09 _ moduleName) -> return [toModuleString moduleName] - (TestSuiteUnsupported _) -> return [] - - getExecutableTarget :: Executable -> IO [String] - getExecutableTarget exe = do - let maybeExes = [p e | p <- P.hsSourceDirs $ P.buildInfo exe, e <- [P.modulePath exe]] - liftIO $ filterM doesFileExist maybeExes diff --git a/Language/Haskell/GhcMod/CabalConfig.hs b/Language/Haskell/GhcMod/CabalConfig.hs deleted file mode 100644 index 2ab3024..0000000 --- a/Language/Haskell/GhcMod/CabalConfig.hs +++ /dev/null @@ -1,171 +0,0 @@ -{-# LANGUAGE RecordWildCards, CPP #-} - --- | This module facilitates extracting information from Cabal's on-disk --- 'LocalBuildInfo' (@dist/setup-config@). -module Language.Haskell.GhcMod.CabalConfig ( - CabalConfig - , cabalConfigDependencies - , cabalConfigFlags - ) where - -import Language.Haskell.GhcMod.Error -import Language.Haskell.GhcMod.GhcPkg -import Language.Haskell.GhcMod.PathsAndFiles -import Language.Haskell.GhcMod.Read -import Language.Haskell.GhcMod.Types -import Language.Haskell.GhcMod.Utils -import Language.Haskell.GhcMod.World - -import qualified Language.Haskell.GhcMod.Cabal16 as C16 -import qualified Language.Haskell.GhcMod.Cabal18 as C18 -import qualified Language.Haskell.GhcMod.Cabal21 as C21 - -#ifndef MIN_VERSION_mtl -#define MIN_VERSION_mtl(x,y,z) 1 -#endif - -import Control.Applicative ((<$>)) -import Control.Monad (void, mplus, when) -#if MIN_VERSION_mtl(2,2,1) -import Control.Monad.Except () -#else -import Control.Monad.Error () -#endif -import Data.List (find,tails,isPrefixOf,isInfixOf,nub,stripPrefix) -import Distribution.Package (InstalledPackageId(..) - , PackageIdentifier(..) - , PackageName(..)) -import Distribution.PackageDescription (FlagAssignment) -import Distribution.Simple.LocalBuildInfo (ComponentName) -import MonadUtils (liftIO) - ----------------------------------------------------------------- - --- | 'Show'ed cabal 'LocalBuildInfo' string -type CabalConfig = String - --- | Get contents of the file containing 'LocalBuildInfo' data. If it doesn't --- exist run @cabal configure@ i.e. configure with default options like @cabal --- build@ would do. -getConfig :: (IOish m, MonadError GhcModError m) - => Cradle - -> m CabalConfig -getConfig cradle = do - outOfDate <- liftIO $ isSetupConfigOutOfDate cradle - when outOfDate configure - liftIO (readFile file) `tryFix` \_ -> - configure `modifyError'` GMECabalConfigure - where - file = setupConfigFile cradle - prjDir = cradleRootDir cradle - - configure :: (IOish m, MonadError GhcModError m) => m () - configure = withDirectory_ prjDir $ void $ readProcess' "cabal" ["configure"] - --- | Get list of 'Package's needed by all components of the current package -cabalConfigDependencies :: (IOish m, MonadError GhcModError m) - => Cradle - -> PackageIdentifier - -> m [Package] -cabalConfigDependencies cradle thisPkg = - configDependencies thisPkg <$> getConfig cradle - --- | Extract list of depencenies for all components from 'CabalConfig' -configDependencies :: PackageIdentifier -> CabalConfig -> [Package] -configDependencies thisPkg config = map fromInstalledPackageId deps - where - deps :: [InstalledPackageId] - deps = case deps21 `mplus` deps18 `mplus` deps16 of - Right ps -> ps - Left msg -> error msg - - -- True if this dependency is an internal one (depends on the library - -- defined in the same package). - internal pkgid = pkgid == thisPkg - - -- Cabal >= 1.21 - deps21 :: Either String [InstalledPackageId] - deps21 = - map fst - <$> filterInternal21 - <$> (readEither =<< extractField config "componentsConfigs") - - filterInternal21 - :: [(ComponentName, C21.ComponentLocalBuildInfo, [ComponentName])] - -> [(InstalledPackageId, C21.PackageIdentifier)] - - filterInternal21 ccfg = [ (ipkgid, pkgid) - | (_,clbi,_) <- ccfg - , (ipkgid, pkgid) <- C21.componentPackageDeps clbi - , not (internal . packageIdentifierFrom21 $ pkgid) ] - - packageIdentifierFrom21 :: C21.PackageIdentifier -> PackageIdentifier - packageIdentifierFrom21 (C21.PackageIdentifier (C21.PackageName myName) myVersion) = - PackageIdentifier (PackageName myName) myVersion - - -- Cabal >= 1.18 && < 1.21 - deps18 :: Either String [InstalledPackageId] - deps18 = - map fst - <$> filterInternal - <$> (readEither =<< extractField config "componentsConfigs") - - filterInternal - :: [(ComponentName, C18.ComponentLocalBuildInfo, [ComponentName])] - -> [(InstalledPackageId, PackageIdentifier)] - - filterInternal ccfg = [ (ipkgid, pkgid) - | (_,clbi,_) <- ccfg - , (ipkgid, pkgid) <- C18.componentPackageDeps clbi - , not (internal pkgid) ] - - -- Cabal 1.16 and below - deps16 :: Either String [InstalledPackageId] - deps16 = map fst <$> filter (not . internal . snd) . nub <$> do - cbi <- concat <$> sequence [ extract "executableConfigs" - , extract "testSuiteConfigs" - , extract "benchmarkConfigs" ] - :: Either String [(String, C16.ComponentLocalBuildInfo)] - - return $ maybe [] C16.componentPackageDeps libraryConfig - ++ concatMap (C16.componentPackageDeps . snd) cbi - where - libraryConfig :: Maybe C16.ComponentLocalBuildInfo - libraryConfig = do - field <- find ("libraryConfig" `isPrefixOf`) (tails config) - clbi <- stripPrefix " = " field - if "Nothing" `isPrefixOf` clbi - then Nothing - else case readMaybe =<< stripPrefix "Just " clbi of - Just x -> x - Nothing -> error $ "reading libraryConfig failed\n" ++ show (stripPrefix "Just " clbi) - - extract :: String -> Either String [(String, C16.ComponentLocalBuildInfo)] - extract field = readConfigs field <$> extractField config field - - readConfigs :: String -> String -> [(String, C16.ComponentLocalBuildInfo)] - readConfigs f s = case readEither s of - Right x -> x - Left msg -> error $ "reading config " ++ f ++ " failed ("++msg++")" - --- | Get the flag assignment from the local build info of the given cradle -cabalConfigFlags :: (IOish m, MonadError GhcModError m) - => Cradle - -> m FlagAssignment -cabalConfigFlags cradle = do - config <- getConfig cradle - case configFlags config of - Right x -> return x - Left msg -> throwError (GMECabalFlags (GMEString msg)) - --- | Extract the cabal flags from the 'CabalConfig' -configFlags :: CabalConfig -> Either String FlagAssignment -configFlags config = readEither =<< flip extractField "configConfigurationsFlags" =<< extractField config "configFlags" - --- | Find @field@ in 'CabalConfig'. Returns 'Left' containing a user readable --- error message with lots of context on failure. -extractField :: CabalConfig -> String -> Either String String -extractField config field = - case extractParens <$> find (field `isPrefixOf`) (tails config) of - Just f -> Right f - Nothing -> Left $ "extractField: failed extracting "++field++" from input, input contained `"++field++"'? " ++ show (field `isInfixOf` config) diff --git a/Language/Haskell/GhcMod/CabalHelper.hs b/Language/Haskell/GhcMod/CabalHelper.hs new file mode 100644 index 0000000..ef6f501 --- /dev/null +++ b/Language/Haskell/GhcMod/CabalHelper.hs @@ -0,0 +1,228 @@ +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +{-# LANGUAGE CPP #-} +module Language.Haskell.GhcMod.CabalHelper +#ifndef SPEC + ( getComponents + , getGhcMergedPkgOptions + , getCabalPackageDbStack + , getCustomPkgDbStack + , prepareCabalHelper + ) +#endif + where + +import Control.Applicative +import Control.Monad +import Control.Category ((.)) +import Data.Maybe +import Data.Monoid +import Data.Serialize (Serialize) +import Data.Traversable +import Distribution.Helper +import qualified Language.Haskell.GhcMod.Types as T +import Language.Haskell.GhcMod.Types hiding (ghcProgram, ghcPkgProgram, + cabalProgram) +import Language.Haskell.GhcMod.Monad.Types +import Language.Haskell.GhcMod.Utils +import Language.Haskell.GhcMod.PathsAndFiles +import Language.Haskell.GhcMod.Logging +import Language.Haskell.GhcMod.Output +import System.FilePath +import Prelude hiding ((.)) + +import Paths_ghc_mod as GhcMod + +-- | Only package related GHC options, sufficient for things that don't need to +-- access home modules +getGhcMergedPkgOptions :: (Applicative m, IOish m, GmEnv m, GmState m, GmLog m) + => m [GHCOption] +getGhcMergedPkgOptions = chCached Cached { + cacheLens = Just (lGmcMergedPkgOptions . lGmCaches), + cacheFile = mergedPkgOptsCacheFile, + cachedAction = \ _tcf (progs, rootdir, distdir, _) _ma -> do + readProc <- gmReadProcess + opts <- withCabal $ runQuery'' readProc progs rootdir distdir $ + ghcMergedPkgOptions + return ([setupConfigPath], opts) + } + +getCabalPackageDbStack :: (IOish m, GmEnv m, GmState m, GmLog m) => m [GhcPkgDb] +getCabalPackageDbStack = chCached Cached { + cacheLens = Just (lGmcPackageDbStack . lGmCaches), + cacheFile = pkgDbStackCacheFile, + cachedAction = \ _tcf (progs, rootdir, distdir, _) _ma -> do + readProc <- gmReadProcess + dbs <- withCabal $ map chPkgToGhcPkg <$> runQuery'' readProc progs rootdir distdir packageDbStack + return ([setupConfigPath, sandboxConfigFile], dbs) + } + +chPkgToGhcPkg :: ChPkgDb -> GhcPkgDb +chPkgToGhcPkg ChPkgGlobal = GlobalDb +chPkgToGhcPkg ChPkgUser = UserDb +chPkgToGhcPkg (ChPkgSpecific f) = PackageDb f + +-- | Primary interface to cabal-helper and intended single entrypoint to +-- constructing 'GmComponent's +-- +-- The Component\'s 'gmcHomeModuleGraph' will be empty and has to be resolved by +-- 'resolveGmComponents'. +getComponents :: (Applicative m, IOish m, GmEnv m, GmState m, GmLog m) + => m [GmComponent 'GMCRaw ChEntrypoint] +getComponents = chCached Cached { + cacheLens = Just (lGmcComponents . lGmCaches), + cacheFile = cabalHelperCacheFile, + cachedAction = \ _tcf (progs, rootdir, distdir, _vers) _ma -> do + readProc <- gmReadProcess + runQuery'' readProc progs rootdir distdir $ do + q <- join7 + <$> ghcOptions + <*> ghcPkgOptions + <*> ghcSrcOptions + <*> ghcLangOptions + <*> entrypoints + <*> entrypoints + <*> sourceDirs + let cs = flip map q $ curry8 (GmComponent mempty) + return ([setupConfigPath], cs) + } + where + curry8 fn (a, (b, (c, (d, (e, (f, (g, h))))))) = fn a b c d e f g h + + join7 a b c d e f = join' a . join' b . join' c . join' d . join' e . join' f + join' :: Eq a => [(a,b)] -> [(a,c)] -> [(a,(b,c))] + join' lb lc = [ (a, (b, c)) + | (a, b) <- lb + , (a', c) <- lc + , a == a' + ] + +prepareCabalHelper :: (IOish m, GmEnv m, GmLog m) => m () +prepareCabalHelper = do + crdl <- cradle + let projdir = cradleRootDir crdl + distdir = projdir "dist" + readProc <- gmReadProcess + when (cradleProjectType crdl == CabalProject) $ + withCabal $ liftIO $ prepare readProc projdir distdir + +parseCustomPackageDb :: String -> [GhcPkgDb] +parseCustomPackageDb src = map parsePkgDb $ filter (not . null) $ lines src + where + parsePkgDb "global" = GlobalDb + parsePkgDb "user" = UserDb + parsePkgDb s = PackageDb s + +getCustomPkgDbStack :: (IOish m, GmEnv m) => m (Maybe [GhcPkgDb]) +getCustomPkgDbStack = do + mCusPkgDbFile <- liftIO . (traverse readFile <=< findCustomPackageDbFile) . cradleRootDir =<< cradle + return $ parseCustomPackageDb <$> mCusPkgDbFile + +withCabal :: (IOish m, GmEnv m, GmLog m) => m a -> m a +withCabal action = do + crdl <- cradle + opts <- options + readProc <- gmReadProcess + + let projdir = cradleRootDir crdl + distdir = projdir "dist" + + mCabalFile <- liftIO $ timeFile `traverse` cradleCabalFile crdl + mCabalConfig <- liftIO $ timeMaybe (setupConfigFile crdl) + + mCusPkgDbStack <- getCustomPkgDbStack + + pkgDbStackOutOfSync <- + case mCusPkgDbStack of + Just cusPkgDbStack -> do + pkgDb <- runQuery'' readProc (helperProgs opts) projdir distdir $ + map chPkgToGhcPkg <$> packageDbStack + return $ pkgDb /= cusPkgDbStack + + Nothing -> return False + + cusPkgStack <- maybe [] ((PackageDb "clear"):) <$> getCustomPkgDbStack + + --TODO: also invalidate when sandboxConfig file changed + + when (isSetupConfigOutOfDate mCabalFile mCabalConfig) $ + gmLog GmDebug "" $ strDoc $ "setup configuration is out of date, reconfiguring Cabal project." + when pkgDbStackOutOfSync $ + gmLog GmDebug "" $ strDoc $ "package-db stack out of sync with ghc-mod.package-db-stack, reconfiguring Cabal project." + + when (isSetupConfigOutOfDate mCabalFile mCabalConfig || pkgDbStackOutOfSync) $ + withDirectory_ (cradleRootDir crdl) $ do + let progOpts = + [ "--with-ghc=" ++ T.ghcProgram opts ] + -- Only pass ghc-pkg if it was actually set otherwise we + -- might break cabal's guessing logic + ++ if T.ghcPkgProgram opts /= T.ghcPkgProgram defaultOptions + then [ "--with-ghc-pkg=" ++ T.ghcPkgProgram opts ] + else [] + ++ map pkgDbArg cusPkgStack + liftIO $ void $ readProc (T.cabalProgram opts) ("configure":progOpts) "" + gmLog GmDebug "" $ strDoc $ "writing Cabal autogen files" + liftIO $ writeAutogenFiles readProc projdir distdir + action + +pkgDbArg :: GhcPkgDb -> String +pkgDbArg GlobalDb = "--package-db=global" +pkgDbArg UserDb = "--package-db=user" +pkgDbArg (PackageDb p) = "--package-db=" ++ p + +-- * Neither file exists -> should return False: +-- @Nothing < Nothing = False@ +-- (since we don't need to @cabal configure@ when no cabal file exists.) +-- +-- * Cabal file doesn't exist (unlikely case) -> should return False +-- @Just cc < Nothing = False@ +-- TODO: should we delete dist/setup-config? +-- +-- * dist/setup-config doesn't exist yet -> should return True: +-- @Nothing < Just cf = True@ +-- +-- * Both files exist +-- @Just cc < Just cf = cc < cf = cc `olderThan` cf@ +isSetupConfigOutOfDate :: Maybe TimedFile -> Maybe TimedFile -> Bool +isSetupConfigOutOfDate worldCabalFile worldCabalConfig = do + worldCabalConfig < worldCabalFile + + +helperProgs :: Options -> Programs +helperProgs opts = Programs { + cabalProgram = T.cabalProgram opts, + ghcProgram = T.ghcProgram opts, + ghcPkgProgram = T.ghcPkgProgram opts + } + +chCached :: (Applicative m, IOish m, GmEnv m, GmState m, GmLog m, Serialize a) + => Cached m GhcModState ChCacheData a -> m a +chCached c = do + root <- cradleRootDir <$> cradle + d <- cacheInputData root + withCabal $ cached root c d + where + cacheInputData root = do + opt <- options + return $ ( helperProgs opt + , root + , root "dist" + , (gmVer, chVer) + ) + + gmVer = GhcMod.version + chVer = VERSION_cabal_helper diff --git a/Language/Haskell/GhcMod/Caching.hs b/Language/Haskell/GhcMod/Caching.hs new file mode 100644 index 0000000..d074a17 --- /dev/null +++ b/Language/Haskell/GhcMod/Caching.hs @@ -0,0 +1,103 @@ +{-# LANGUAGE OverloadedStrings #-} +module Language.Haskell.GhcMod.Caching ( + module Language.Haskell.GhcMod.Caching + , module Language.Haskell.GhcMod.Caching.Types + ) where + +import Control.Arrow (first) +import Control.Monad +import Control.Monad.Trans.Maybe +import Data.Maybe +import Data.Serialize (Serialize, encode, decode) +import Data.Version +import Data.Label +import qualified Data.ByteString as BS +import qualified Data.ByteString.Char8 as BS8 +import System.FilePath +import Utils (TimedFile(..), timeMaybe, mightExist) +import Paths_ghc_mod (version) + +import Language.Haskell.GhcMod.Monad.Types +import Language.Haskell.GhcMod.Caching.Types +import Language.Haskell.GhcMod.Logging + +-- | Cache a MonadIO action with proper invalidation. +cached :: forall m a d. (Gm m, MonadIO m, Serialize a, Eq d, Serialize d, Show d) + => FilePath -- ^ Directory to prepend to 'cacheFile' + -> Cached m GhcModState d a -- ^ Cache descriptor + -> d + -> m a +cached dir cd d = do + mcc <- readCache + tcfile <- liftIO $ timeMaybe (cacheFile cd) + case mcc of + Nothing -> + writeCache (TimedCacheFiles tcfile []) Nothing "cache missing or unreadable" + Just (ifs, d', a) | d /= d' -> do + tcf <- timeCacheInput dir (cacheFile cd) ifs + writeCache tcf (Just a) $ "input data changed" -- ++ " was: " ++ show d ++ " is: " ++ show d' + Just (ifs, _, a) -> do + tcf <- timeCacheInput dir (cacheFile cd) ifs + case invalidatingInputFiles tcf of + Just [] -> return a + Just _ -> writeCache tcf (Just a) "input files changed" + Nothing -> writeCache tcf (Just a) "cache missing, existed a sec ago WTF?" + + where + cacheHeader = BS8.pack $ "Written by ghc-mod " ++ showVersion version ++ "\n" + + writeCache tcf ma cause = do + (ifs', a) <- (cachedAction cd) tcf d ma + gmLog GmDebug "" $ (text "regenerating cache") <+>: text (cacheFile cd) + <+> parens (text cause) + case cacheLens cd of + Nothing -> return () + Just label -> do + gmLog GmDebug "" $ (text "writing memory cache") <+>: text (cacheFile cd) + setLabel label $ Just (ifs', d, a) + + liftIO $ BS.writeFile (dir cacheFile cd) $ + BS.append cacheHeader $ encode (ifs', d, a) + return a + + setLabel l x = do + s <- gmsGet + gmsPut $ set l x s + + readCache :: m (Maybe ([FilePath], d, a)) + readCache = runMaybeT $ do + case cacheLens cd of + Just label -> do + c <- MaybeT (get label `liftM` gmsGet) `mplus` readCacheFromFile + setLabel label $ Just c + return c + Nothing -> + readCacheFromFile + + readCacheFromFile = do + f <- MaybeT $ liftIO $ mightExist $ cacheFile cd + readCacheFromFile' f + + readCacheFromFile' f = MaybeT $ do + gmLog GmDebug "" $ (text "reading cache") <+>: text (cacheFile cd) + cc <- liftIO $ BS.readFile f + case first BS8.words $ BS8.span (/='\n') cc of + (["Written", "by", "ghc-mod", ver], rest) + | BS8.unpack ver == showVersion version -> + return $ either (const Nothing) Just $ decode $ BS.drop 1 rest + _ -> return Nothing + +timeCacheInput :: MonadIO m => FilePath -> FilePath -> [FilePath] -> m TimedCacheFiles +timeCacheInput dir cfile ifs = liftIO $ do + -- TODO: is checking the times this way around race free? + ins <- (timeMaybe . (dir )) `mapM` ifs + mtcfile <- timeMaybe cfile + return $ TimedCacheFiles mtcfile (catMaybes ins) + +invalidatingInputFiles :: TimedCacheFiles -> Maybe [FilePath] +invalidatingInputFiles tcf = + case tcCacheFile tcf of + Nothing -> Nothing + Just tcfile -> Just $ map tfPath $ + -- get input files older than tcfile + filter (tcfile<) $ tcFiles tcf diff --git a/Language/Haskell/GhcMod/Caching/Types.hs b/Language/Haskell/GhcMod/Caching/Types.hs new file mode 100644 index 0000000..ae32a7c --- /dev/null +++ b/Language/Haskell/GhcMod/Caching/Types.hs @@ -0,0 +1,52 @@ +module Language.Haskell.GhcMod.Caching.Types where + +import Utils +import Data.Label +import Data.Version +import Distribution.Helper + +type CacheContents d a = Maybe ([FilePath], d, a) +type CacheLens s d a = s :-> CacheContents d a + +data Cached m s d a = Cached { + cacheFile :: FilePath, + cacheLens :: Maybe (CacheLens s d a), + cachedAction :: TimedCacheFiles + -> d + -> Maybe a + -> m ([FilePath], a) + + -- ^ @cachedAction tcf data ma@ + -- + -- * @tcf@: Input file timestamps. Not technically necessary, just an + -- optimizazion when knowing which input files changed can make updating the + -- cache faster + -- + -- * @data@: Arbitrary static input data to cache action. Can be used to + -- invalidate the cache using something other than file timestamps + -- i.e. environment tool version numbers + -- + -- * @ma@: Cached data if it existed + -- + -- Returns: + -- + -- * @fst@: Input files used in generating the cache + -- + -- * @snd@: Cache data, will be stored alongside the static input data in the + -- 'cacheFile' + -- + -- The cached action, will only run if one of the following is true: + -- + -- * 'cacheFile' doesn\'t exist yet + -- * 'cacheFile' exists and 'inputData' changed + -- * any files returned by the cached action changed + } + +data TimedCacheFiles = TimedCacheFiles { + tcCacheFile :: Maybe TimedFile, + -- ^ 'cacheFile' timestamp + tcFiles :: [TimedFile] + -- ^ Timestamped files returned by the cached action + } + +type ChCacheData = (Programs, FilePath, FilePath, (Version, [Char])) diff --git a/Language/Haskell/GhcMod/CaseSplit.hs b/Language/Haskell/GhcMod/CaseSplit.hs index dabb67b..77603ca 100644 --- a/Language/Haskell/GhcMod/CaseSplit.hs +++ b/Language/Haskell/GhcMod/CaseSplit.hs @@ -8,17 +8,24 @@ import Data.List (find, intercalate) import Data.Maybe (isJust) import qualified Data.Text as T import qualified Data.Text.IO as T (readFile) +import System.FilePath + import qualified DataCon as Ty -import Exception (ghandle, SomeException(..)) import GHC (GhcMonad, LPat, Id, ParsedModule(..), TypecheckedModule(..), DynFlags, SrcSpan, Type, GenLocated(L)) import qualified GHC as G -import Language.Haskell.GhcMod.Convert -import qualified Language.Haskell.GhcMod.Gap as Gap -import Language.Haskell.GhcMod.Monad -import Language.Haskell.GhcMod.SrcUtils import Outputable (PprStyle) import qualified TyCon as Ty import qualified Type as Ty +import Exception + +import Language.Haskell.GhcMod.Convert +import Language.Haskell.GhcMod.DynFlags +import qualified Language.Haskell.GhcMod.Gap as Gap +import Language.Haskell.GhcMod.Monad +import Language.Haskell.GhcMod.SrcUtils +import Language.Haskell.GhcMod.Doc +import Language.Haskell.GhcMod.Logging +import Language.Haskell.GhcMod.Types ---------------------------------------------------------------- -- CASE SPLITTING @@ -38,23 +45,29 @@ splits :: IOish m -> Int -- ^ Line number. -> Int -- ^ Column number. -> GhcModT m String -splits file lineNo colNo = ghandle handler body - where - body = inModuleContext file $ \dflag style -> do - opt <- options - modSum <- Gap.fileModSummary file - whenFound' opt (getSrcSpanTypeForSplit modSum lineNo colNo) $ \x -> case x of - (SplitInfo varName bndLoc (varLoc,varT) _matches) -> do - let varName' = showName dflag style varName -- Convert name to string - text <- genCaseSplitTextFile file (SplitToTextInfo varName' bndLoc varLoc $ - getTyCons dflag style varName varT) - return (fourInts bndLoc, text) - (TySplitInfo varName bndLoc (varLoc,varT)) -> do - let varName' = showName dflag style varName -- Convert name to string - text <- genCaseSplitTextFile file (SplitToTextInfo varName' bndLoc varLoc $ - getTyCons dflag style varName varT) - return (fourInts bndLoc, text) - handler (SomeException _) = emptyResult =<< options +splits file lineNo colNo = + ghandle handler $ runGmlT' [Left file] deferErrors $ do + opt <- options + crdl <- cradle + style <- getStyle + dflag <- G.getSessionDynFlags + modSum <- Gap.fileModSummary (cradleCurrentDir crdl file) + whenFound' opt (getSrcSpanTypeForSplit modSum lineNo colNo) $ \x -> case x of + (SplitInfo varName bndLoc (varLoc,varT) _matches) -> do + let varName' = showName dflag style varName -- Convert name to string + t <- genCaseSplitTextFile file (SplitToTextInfo varName' bndLoc varLoc $ + getTyCons dflag style varName varT) + return (fourInts bndLoc, t) + (TySplitInfo varName bndLoc (varLoc,varT)) -> do + let varName' = showName dflag style varName -- Convert name to string + t <- genCaseSplitTextFile file (SplitToTextInfo varName' bndLoc varLoc $ + getTyCons dflag style varName varT) + return (fourInts bndLoc, t) + where + handler (SomeException ex) = do + gmLog GmDebug "splits" $ + text "" $$ nest 4 (showDoc ex) + emptyResult =<< options ---------------------------------------------------------------- -- a. Code for getting the information of the variable @@ -79,7 +92,11 @@ getSrcSpanTypeForFnSplit modSum lineNo colNo = do varT <- Gap.getType tcm varPat' -- Finally we get the type of the var case varT of Just varT' -> +#if __GLASGOW_HASKELL__ >= 710 + let (L matchL (G.Match _ _ _ (G.GRHSs rhsLs _))) = match +#else let (L matchL (G.Match _ _ (G.GRHSs rhsLs _))) = match +#endif in return $ Just (SplitInfo (getPatternVarName varPat') matchL varT' (map G.getLoc rhsLs) ) _ -> return Nothing @@ -178,15 +195,16 @@ showFieldNames dflag style v (x:xs) = let fName = showName dflag style x ---------------------------------------------------------------- -- c. Code for performing the case splitting -genCaseSplitTextFile :: GhcMonad m => FilePath -> SplitToTextInfo -> m String +genCaseSplitTextFile :: (MonadIO m, GhcMonad m) => + FilePath -> SplitToTextInfo -> m String genCaseSplitTextFile file info = liftIO $ do - text <- T.readFile file - return $ getCaseSplitText (T.lines text) info + t <- T.readFile file + return $ getCaseSplitText (T.lines t) info getCaseSplitText :: [T.Text] -> SplitToTextInfo -> String -getCaseSplitText text (SplitToTextInfo { sVarName = sVN, sBindingSpan = sBS +getCaseSplitText t (SplitToTextInfo { sVarName = sVN, sBindingSpan = sBS , sVarSpan = sVS, sTycons = sT }) = - let bindingText = getBindingText text sBS + let bindingText = getBindingText t sBS difference = srcSpanDifference sBS sVS replaced = map (replaceVarWithTyCon bindingText difference sVN) sT -- The newly generated bindings need to be indented to align with the @@ -195,9 +213,9 @@ getCaseSplitText text (SplitToTextInfo { sVarName = sVN, sBindingSpan = sBS in T.unpack $ T.intercalate (T.pack "\n") (concat replaced') getBindingText :: [T.Text] -> SrcSpan -> [T.Text] -getBindingText text srcSpan = +getBindingText t srcSpan = let Just (sl,sc,el,ec) = Gap.getSrcSpan srcSpan - lines_ = drop (sl - 1) $ take el text + lines_ = drop (sl - 1) $ take el t in if sl == el then -- only one line [T.drop (sc - 1) $ T.take ec $ head lines_] @@ -212,7 +230,7 @@ srcSpanDifference b v = in (vsl - bsl, vsc - bsc, vel - bsl, vec - bsc) -- assume variable in one line replaceVarWithTyCon :: [T.Text] -> (Int,Int,Int,Int) -> String -> String -> [T.Text] -replaceVarWithTyCon text (vsl,vsc,_,vec) varname tycon = +replaceVarWithTyCon t (vsl,vsc,_,vec) varname tycon = let tycon' = if ' ' `elem` tycon || ':' `elem` tycon then "(" ++ tycon ++ ")" else tycon lengthDiff = length tycon' - length varname tycon'' = T.pack $ if lengthDiff < 0 then tycon' ++ replicate (-lengthDiff) ' ' else tycon' @@ -222,7 +240,7 @@ replaceVarWithTyCon text (vsl,vsc,_,vec) varname tycon = else if n == vsl then T.take vsc line `T.append` tycon'' `T.append` T.drop vec line else T.replicate spacesToAdd (T.pack " ") `T.append` line) - [0 ..] text + [0 ..] t indentBindingTo :: SrcSpan -> [T.Text] -> [T.Text] indentBindingTo bndLoc binds = diff --git a/Language/Haskell/GhcMod/Check.hs b/Language/Haskell/GhcMod/Check.hs index ce8877f..f4bd658 100644 --- a/Language/Haskell/GhcMod/Check.hs +++ b/Language/Haskell/GhcMod/Check.hs @@ -5,12 +5,12 @@ module Language.Haskell.GhcMod.Check ( , expand ) where -import Control.Applicative ((<$>)) +import Control.Applicative +import Prelude import Language.Haskell.GhcMod.DynFlags import qualified Language.Haskell.GhcMod.Gap as Gap import Language.Haskell.GhcMod.Logger -import Language.Haskell.GhcMod.Monad (IOish, GhcModT) -import Language.Haskell.GhcMod.Target (setTargetFiles) +import Language.Haskell.GhcMod.Monad ---------------------------------------------------------------- @@ -29,15 +29,12 @@ checkSyntax files = either id id <$> check files check :: IOish m => [FilePath] -- ^ The target files. -> GhcModT m (Either String String) -{- -check fileNames = overrideGhcUserOptions $ \ghcOpts -> do - withLogger (setAllWarningFlags . setNoMaxRelevantBindings . Gap.setWarnTypedHoles . Gap.setDeferTypeErrors) $ do - _ <- G.setSessionDynFlags =<< addCmdOpts ghcOpts =<< G.getSessionDynFlags - setTargetFiles fileNames --} -check fileNames = - withLogger (setAllWarningFlags . setNoMaxRelevantBindings) $ - setTargetFiles fileNames +check files = + runGmlTWith + (map Left files) + return + ((fmap fst <$>) . withLogger setNoMaxRelevantBindings) + (return ()) ---------------------------------------------------------------- @@ -51,8 +48,10 @@ expandTemplate files = either id id <$> expand files ---------------------------------------------------------------- -- | Expanding Haskell Template. -expand :: IOish m - => [FilePath] -- ^ The target files. - -> GhcModT m (Either String String) -expand fileNames = withLogger (Gap.setDumpSplices . setNoWarningFlags) $ - setTargetFiles fileNames +expand :: IOish m => [FilePath] -> GhcModT m (Either String String) +expand files = + runGmlTWith + (map Left files) + return + ((fmap fst <$>) . withLogger (Gap.setDumpSplices . setNoWarningFlags)) + (return ()) diff --git a/Language/Haskell/GhcMod/Convert.hs b/Language/Haskell/GhcMod/Convert.hs index 0b53f3b..2715696 100644 --- a/Language/Haskell/GhcMod/Convert.hs +++ b/Language/Haskell/GhcMod/Convert.hs @@ -1,11 +1,12 @@ -{-# LANGUAGE FlexibleInstances, FlexibleContexts, OverlappingInstances #-} +{-# LANGUAGE CPP, FlexibleInstances, FlexibleContexts #-} module Language.Haskell.GhcMod.Convert (convert, convert', emptyResult, whenFound, whenFound') where -import Language.Haskell.GhcMod.Monad +import Language.Haskell.GhcMod.Monad.Types import Language.Haskell.GhcMod.Types -import Control.Applicative ((<$>)) +import Control.Applicative +import Prelude type Builder = String -> String @@ -23,11 +24,11 @@ inter :: Char -> [Builder] -> Builder inter _ [] = id inter c bs = foldr1 (\x y -> x . (c:) . y) bs -convert' :: (ToString a, IOish m) => a -> GhcModT m String +convert' :: (ToString a, IOish m, GmEnv m) => a -> m String convert' x = flip convert x <$> options convert :: ToString a => Options -> a -> String -convert opt@Options { outputStyle = LispStyle } x = toLisp opt x "\n" +convert opt@Options { outputStyle = LispStyle } x = toLisp opt x "\n" convert opt@Options { outputStyle = PlainStyle } x | str == "\n" = "" | otherwise = str @@ -35,8 +36,8 @@ convert opt@Options { outputStyle = PlainStyle } x str = toPlain opt x "\n" class ToString a where - toLisp :: Options -> a -> Builder - toPlain :: Options -> a -> Builder + toLisp :: Options -> a -> Builder + toPlain :: Options -> a -> Builder lineSep :: Options -> String lineSep opt = interpret lsep @@ -51,8 +52,8 @@ lineSep opt = interpret lsep -- >>> toPlain defaultOptions "foo" "" -- "foo" instance ToString String where - toLisp opt = quote opt - toPlain opt = replace '\n' (lineSep opt) + toLisp opt = quote opt + toPlain opt = replace '\n' (lineSep opt) -- | -- @@ -61,8 +62,12 @@ instance ToString String where -- >>> toPlain defaultOptions ["foo", "bar", "baz"] "" -- "foo\nbar\nbaz" instance ToString [String] where - toLisp opt = toSexp1 opt - toPlain opt = inter '\n' . map (toPlain opt) + toLisp opt = toSexp1 opt + toPlain opt = inter '\n' . map (toPlain opt) + +instance ToString [ModuleString] where + toLisp opt = toLisp opt . map getModuleString + toPlain opt = toPlain opt . map getModuleString -- | -- @@ -72,23 +77,23 @@ instance ToString [String] where -- >>> toPlain defaultOptions inp "" -- "1 2 3 4 \"foo\"\n5 6 7 8 \"bar\"" instance ToString [((Int,Int,Int,Int),String)] where - toLisp opt = toSexp2 . map toS - where - toS x = ('(' :) . tupToString opt x . (')' :) - toPlain opt = inter '\n' . map (tupToString opt) + toLisp opt = toSexp2 . map toS + where + toS x = ('(' :) . tupToString opt x . (')' :) + toPlain opt = inter '\n' . map (tupToString opt) instance ToString ((Int,Int,Int,Int),String) where - toLisp opt x = ('(' :) . tupToString opt x . (')' :) - toPlain opt x = tupToString opt x + toLisp opt x = ('(' :) . tupToString opt x . (')' :) + toPlain opt x = tupToString opt x instance ToString ((Int,Int,Int,Int),[String]) where - toLisp opt (x,s) = ('(' :) . fourIntsToString opt x . - (' ' :) . toLisp opt s . (')' :) - toPlain opt (x,s) = fourIntsToString opt x . ('\n' :) . toPlain opt s + toLisp opt (x,s) = ('(' :) . fourIntsToString opt x . + (' ' :) . toLisp opt s . (')' :) + toPlain opt (x,s) = fourIntsToString opt x . ('\n' :) . toPlain opt s instance ToString (String, (Int,Int,Int,Int),[String]) where - toLisp opt (s,x,y) = toSexp2 [toLisp opt s, ('(' :) . fourIntsToString opt x . (')' :), toLisp opt y] - toPlain opt (s,x,y) = inter '\n' [toPlain opt s, fourIntsToString opt x, toPlain opt y] + toLisp opt (s,x,y) = toSexp2 [toLisp opt s, ('(' :) . fourIntsToString opt x . (')' :), toLisp opt y] + toPlain opt (s,x,y) = inter '\n' [toPlain opt s, fourIntsToString opt x, toPlain opt y] toSexp1 :: Options -> [String] -> Builder toSexp1 opt ss = ('(' :) . inter ' ' (map (quote opt) ss) . (')' :) diff --git a/Language/Haskell/GhcMod/Cradle.hs b/Language/Haskell/GhcMod/Cradle.hs index 94ee836..78e041d 100644 --- a/Language/Haskell/GhcMod/Cradle.hs +++ b/Language/Haskell/GhcMod/Cradle.hs @@ -1,19 +1,22 @@ module Language.Haskell.GhcMod.Cradle ( findCradle , findCradle' - , findCradleWithoutSandbox + , findSpecCradle , cleanupCradle ) where -import Language.Haskell.GhcMod.GhcPkg import Language.Haskell.GhcMod.PathsAndFiles +import Language.Haskell.GhcMod.Monad.Types import Language.Haskell.GhcMod.Types import Language.Haskell.GhcMod.Utils -import Control.Exception.IOChoice ((||>)) -import System.Directory (getCurrentDirectory, removeDirectoryRecursive) -import System.FilePath (takeDirectory) - +import Control.Applicative +import Control.Monad +import Control.Monad.Trans.Maybe +import Data.Maybe +import System.Directory +import System.FilePath +import Prelude ---------------------------------------------------------------- @@ -25,51 +28,62 @@ findCradle :: IO Cradle findCradle = findCradle' =<< getCurrentDirectory findCradle' :: FilePath -> IO Cradle -findCradle' dir = cabalCradle dir ||> sandboxCradle dir ||> plainCradle dir +findCradle' dir = run $ do + (cabalCradle dir `mplus` sandboxCradle dir `mplus` plainCradle dir) + where run a = fillTempDir =<< (fromJust <$> runMaybeT a) + +findSpecCradle :: FilePath -> IO Cradle +findSpecCradle dir = do + let cfs = [cabalCradle, sandboxCradle] + cs <- catMaybes <$> mapM (runMaybeT . ($ dir)) cfs + gcs <- filterM isNotGmCradle cs + fillTempDir =<< case gcs of + [] -> fromJust <$> runMaybeT (plainCradle dir) + c:_ -> return c + where + isNotGmCradle :: Cradle -> IO Bool + isNotGmCradle crdl = do + not <$> doesFileExist (cradleRootDir crdl "ghc-mod.cabal") cleanupCradle :: Cradle -> IO () cleanupCradle crdl = removeDirectoryRecursive $ cradleTempDir crdl -cabalCradle :: FilePath -> IO Cradle +fillTempDir :: MonadIO m => Cradle -> m Cradle +fillTempDir crdl = do + tmpDir <- liftIO $ newTempDir (cradleRootDir crdl) + return crdl { cradleTempDir = tmpDir } + +cabalCradle :: FilePath -> MaybeT IO Cradle cabalCradle wdir = do - Just cabalFile <- findCabalFile wdir + cabalFile <- MaybeT $ findCabalFile wdir + let cabalDir = takeDirectory cabalFile - pkgDbStack <- getPackageDbStack cabalDir - tmpDir <- newTempDir cabalDir + return Cradle { - cradleCurrentDir = wdir + cradleProjectType = CabalProject + , cradleCurrentDir = wdir , cradleRootDir = cabalDir - , cradleTempDir = tmpDir + , cradleTempDir = error "tmpDir" , cradleCabalFile = Just cabalFile - , cradlePkgDbStack = pkgDbStack } -sandboxCradle :: FilePath -> IO Cradle +sandboxCradle :: FilePath -> MaybeT IO Cradle sandboxCradle wdir = do - Just sbDir <- getSandboxDb wdir - pkgDbStack <- getPackageDbStack sbDir - tmpDir <- newTempDir sbDir + sbDir <- MaybeT $ findCabalSandboxDir wdir return Cradle { - cradleCurrentDir = wdir + cradleProjectType = SandboxProject + , cradleCurrentDir = wdir , cradleRootDir = sbDir - , cradleTempDir = tmpDir + , cradleTempDir = error "tmpDir" , cradleCabalFile = Nothing - , cradlePkgDbStack = pkgDbStack } -plainCradle :: FilePath -> IO Cradle +plainCradle :: FilePath -> MaybeT IO Cradle plainCradle wdir = do - tmpDir <- newTempDir wdir - return Cradle { - cradleCurrentDir = wdir + return $ Cradle { + cradleProjectType = PlainProject + , cradleCurrentDir = wdir , cradleRootDir = wdir - , cradleTempDir = tmpDir + , cradleTempDir = error "tmpDir" , cradleCabalFile = Nothing - , cradlePkgDbStack = [GlobalDb, UserDb] } - --- Just for testing -findCradleWithoutSandbox :: IO Cradle -findCradleWithoutSandbox = do - cradle <- findCradle - return cradle { cradlePkgDbStack = [GlobalDb]} -- FIXME diff --git a/Language/Haskell/GhcMod/Debug.hs b/Language/Haskell/GhcMod/Debug.hs index f1382ce..54e85d2 100644 --- a/Language/Haskell/GhcMod/Debug.hs +++ b/Language/Haskell/GhcMod/Debug.hs @@ -1,39 +1,127 @@ -module Language.Haskell.GhcMod.Debug (debugInfo, rootInfo) where +module Language.Haskell.GhcMod.Debug (debugInfo, rootInfo, componentInfo) where -import Control.Applicative ((<$>)) -import Data.List (intercalate) -import Data.Maybe (isJust, fromJust) +import Control.Arrow (first) +import Control.Applicative +import Control.Monad +import qualified Data.Map as Map +import qualified Data.Set as Set +import Data.Char +import Data.List.Split +import Text.PrettyPrint import Language.Haskell.GhcMod.Convert import Language.Haskell.GhcMod.Monad import Language.Haskell.GhcMod.Types import Language.Haskell.GhcMod.Internal +import Language.Haskell.GhcMod.Target +import Language.Haskell.GhcMod.Pretty +import Language.Haskell.GhcMod.Utils ---------------------------------------------------------------- -- | Obtaining debug information. debugInfo :: IOish m => GhcModT m String -debugInfo = cradle >>= \c -> convert' =<< do - CompilerOptions gopts incDir pkgs <- - if isJust $ cradleCabalFile c then - fromCabalFile c ||> simpleCompilerOption - else - simpleCompilerOption - return [ - "Root directory: " ++ cradleRootDir c - , "Current directory: " ++ cradleCurrentDir c - , "Cabal file: " ++ show (cradleCabalFile c) - , "GHC options: " ++ unwords gopts - , "Include directories: " ++ unwords incDir - , "Dependent packages: " ++ intercalate ", " (map showPkg pkgs) - , "System libraries: " ++ ghcLibDir - ] - where - simpleCompilerOption = options >>= \op -> - return $ CompilerOptions (ghcUserOptions op) [] [] - fromCabalFile c = options >>= \opts -> do - pkgDesc <- parseCabalFile c $ fromJust $ cradleCabalFile c - getCompilerOptions (ghcUserOptions opts) c pkgDesc +debugInfo = do + Options {..} <- options + Cradle {..} <- cradle + cabal <- + case cradleProjectType of + CabalProject -> cabalDebug + _ -> return [] + + pkgOpts <- packageGhcOptions + + return $ unlines $ + [ "Root directory: " ++ cradleRootDir + , "Current directory: " ++ cradleCurrentDir + , "GHC Package flags:\n" ++ render (nest 4 $ + fsep $ map text pkgOpts) + , "GHC System libraries: " ++ ghcLibDir + , "GHC user options:\n" ++ render (nest 4 $ + fsep $ map text ghcUserOptions) + ] ++ cabal + +cabalDebug :: IOish m => GhcModT m [String] +cabalDebug = do + Cradle {..} <- cradle + mcs <- cabalResolvedComponents + let entrypoints = Map.map gmcEntrypoints mcs + graphs = Map.map gmcHomeModuleGraph mcs + opts = Map.map gmcGhcOpts mcs + srcOpts = Map.map gmcGhcSrcOpts mcs + + return $ + [ "Cabal file: " ++ show cradleCabalFile + , "Cabal entrypoints:\n" ++ render (nest 4 $ + mapDoc gmComponentNameDoc smpDoc entrypoints) + , "Cabal components:\n" ++ render (nest 4 $ + mapDoc gmComponentNameDoc graphDoc graphs) + , "GHC Cabal options:\n" ++ render (nest 4 $ + mapDoc gmComponentNameDoc (fsep . map text) opts) + , "GHC search path options:\n" ++ render (nest 4 $ + mapDoc gmComponentNameDoc (fsep . map text) srcOpts) + ] + +componentInfo :: IOish m => [String] -> GhcModT m String +componentInfo ts = do + -- TODO: most of this is copypasta of targetGhcOptions. Factor out more + -- useful function from there. + crdl <- cradle + sefnmn <- Set.fromList `liftM` mapM guessModuleFile ts + mcs <- cabalResolvedComponents + let + mdlcs = moduleComponents mcs `zipMap` Set.toList sefnmn + candidates = findCandidates $ map snd mdlcs + cn = pickComponent candidates + opts <- targetGhcOptions crdl sefnmn + + return $ unlines $ + [ "Matching Components:\n" ++ render (nest 4 $ + alistDoc (either text mnDoc) (setDoc gmComponentNameDoc) mdlcs) + , "Picked Component:\n" ++ render (nest 4 $ + gmComponentNameDoc cn) + , "GHC Cabal options:\n" ++ render (nest 4 $ fsep $ map text opts) + ] + where + zipMap f l = l `zip` (f `map` l) + +guessModuleFile :: MonadIO m => String -> m (Either FilePath ModuleName) +guessModuleFile m + | (isUpper . head .&&. (all $ all $ isAlphaNum .||. (=='.')) . splitOn ".") m = + return $ Right $ mkModuleName m + where + infixr 1 .||. + infixr 2 .&&. + (.||.) = liftA2 (||) + (.&&.) = liftA2 (&&) + +guessModuleFile str = Left `liftM` liftIO (canonFilePath str) + +graphDoc :: GmModuleGraph -> Doc +graphDoc GmModuleGraph{..} = + mapDoc mpDoc smpDoc' gmgGraph + where + smpDoc' smp = vcat $ map mpDoc' $ Set.toList smp + mpDoc' = text . moduleNameString . mpModule + +setDoc :: (a -> Doc) -> Set.Set a -> Doc +setDoc f s = vcat $ map f $ Set.toList s + +smpDoc :: Set.Set ModulePath -> Doc +smpDoc smp = setDoc mpDoc smp + +mpDoc :: ModulePath -> Doc +mpDoc (ModulePath mn fn) = text (moduleNameString mn) <+> parens (text fn) + +mnDoc :: ModuleName -> Doc +mnDoc mn = text (moduleNameString mn) + +alistDoc :: Ord k => (k -> Doc) -> (a -> Doc) -> [(k, a)] -> Doc +alistDoc fk fa alist = mapDoc fk fa (Map.fromList alist) + +mapDoc :: (k -> Doc) -> (a -> Doc) -> Map.Map k a -> Doc +mapDoc kd ad m = vcat $ + map (uncurry ($+$)) $ map (first kd) $ Map.toList $ Map.map (nest 4 . ad) m ---------------------------------------------------------------- -- | Obtaining root information. diff --git a/Language/Haskell/GhcMod/Doc.hs b/Language/Haskell/GhcMod/Doc.hs index bbc6b77..823e19b 100644 --- a/Language/Haskell/GhcMod/Doc.hs +++ b/Language/Haskell/GhcMod/Doc.hs @@ -1,9 +1,8 @@ module Language.Haskell.GhcMod.Doc where -import GHC (DynFlags, GhcMonad) -import qualified GHC as G +import GHC import Language.Haskell.GhcMod.Gap (withStyle, showDocWith) -import Outputable (SDoc, PprStyle, mkUserStyle, Depth(AllTheWay), neverQualify) +import Outputable import Pretty (Mode(..)) showPage :: DynFlags -> PprStyle -> SDoc -> String @@ -12,9 +11,14 @@ showPage dflag style = showDocWith dflag PageMode . withStyle dflag style showOneLine :: DynFlags -> PprStyle -> SDoc -> String showOneLine dflag style = showDocWith dflag OneLineMode . withStyle dflag style +-- showForUser :: DynFlags -> PrintUnqualified -> SDoc -> String +-- showForUser dflags unqual sdoc = +-- showDocWith dflags PageMode $ +-- runSDoc sdoc $ initSDocContext dflags $ mkUserStyle unqual AllTheWay + getStyle :: GhcMonad m => m PprStyle getStyle = do - unqual <- G.getPrintUnqual + unqual <- getPrintUnqual return $ mkUserStyle unqual AllTheWay styleUnqualified :: PprStyle diff --git a/Language/Haskell/GhcMod/DynFlags.hs b/Language/Haskell/GhcMod/DynFlags.hs index 5350f16..f1950f7 100644 --- a/Language/Haskell/GhcMod/DynFlags.hs +++ b/Language/Haskell/GhcMod/DynFlags.hs @@ -2,7 +2,7 @@ module Language.Haskell.GhcMod.DynFlags where -import Control.Applicative ((<$>)) +import Control.Applicative import Control.Monad (void) import GHC (DynFlags(..), GhcMode(..), GhcLink(..), HscTarget(..)) import qualified GHC as G @@ -11,8 +11,7 @@ import GhcMonad import qualified Language.Haskell.GhcMod.Gap as Gap import Language.Haskell.GhcMod.Types import System.IO.Unsafe (unsafePerformIO) - -data Build = CabalPkg | SingleFile deriving Eq +import Prelude setEmptyLogger :: DynFlags -> DynFlags setEmptyLogger df = Gap.setLogAction df $ \_ _ _ _ _ -> return () @@ -41,37 +40,15 @@ setModeIntelligent df = df { , optLevel = 0 } -setIncludeDirs :: [IncludeDir] -> DynFlags -> DynFlags -setIncludeDirs idirs df = df { importPaths = idirs } - -setBuildEnv :: Build -> DynFlags -> DynFlags -setBuildEnv build = setHideAllPackages build . setCabalPackage build - --- | With ghc-7.8 this option simply makes GHC print a message suggesting users --- add hiddend packages to the build-depends field in their cabal file when the --- user tries to import a module form a hidden package. -setCabalPackage :: Build -> DynFlags -> DynFlags -setCabalPackage CabalPkg df = Gap.setCabalPkg df -setCabalPackage _ df = df - --- | Enable hiding of all package not explicitly exposed (like Cabal does) -setHideAllPackages :: Build -> DynFlags -> DynFlags -setHideAllPackages CabalPkg df = Gap.setHideAllPackages df -setHideAllPackages _ df = df - -- | Parse command line ghc options and add them to the 'DynFlags' passed addCmdOpts :: GhcMonad m => [GHCOption] -> DynFlags -> m DynFlags addCmdOpts cmdOpts df = - tfst <$> G.parseDynamicFlags df (map G.noLoc cmdOpts) + fst3 <$> G.parseDynamicFlags df (map G.noLoc cmdOpts) where - tfst (a,_,_) = a + fst3 (a,_,_) = a ---------------------------------------------------------------- --- | Return the 'DynFlags' currently in use in the GHC session. -getDynamicFlags :: IO DynFlags -getDynamicFlags = G.runGhc (Just libdir) G.getSessionDynFlags - withDynFlags :: GhcMonad m => (DynFlags -> DynFlags) -> m a @@ -119,3 +96,7 @@ setNoMaxRelevantBindings df = df { maxRelevantBinds = Nothing } #else setNoMaxRelevantBindings = id #endif + +deferErrors :: DynFlags -> Ghc DynFlags +deferErrors df = return $ + Gap.setWarnTypedHoles $ Gap.setDeferTypeErrors $ setNoWarningFlags df diff --git a/Language/Haskell/GhcMod/Error.hs b/Language/Haskell/GhcMod/Error.hs index 9fa2b80..965aa7e 100644 --- a/Language/Haskell/GhcMod/Error.hs +++ b/Language/Haskell/GhcMod/Error.hs @@ -1,45 +1,88 @@ -{-# LANGUAGE TypeFamilies, ScopedTypeVariables, DeriveDataTypeable #-} +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +{-# LANGUAGE ExistentialQuantification #-} module Language.Haskell.GhcMod.Error ( GhcModError(..) + , GMConfigStateFileError(..) + , GmError , gmeDoc + , ghcExceptionDoc + , liftMaybe + , overrideError , modifyError , modifyError' + , modifyGmError , tryFix + , GHandler(..) + , gcatches , module Control.Monad.Error - , module Exception + , module Control.Exception ) where -import Control.Monad.Error (MonadError(..), Error(..)) +import Control.Arrow +import Control.Exception +import Control.Monad.Error hiding (MonadIO, liftIO) +import qualified Data.Set as Set import Data.List -import Data.Typeable -import Exception +import Data.Version +import System.Process (showCommandForUser) import Text.PrettyPrint +import Text.Printf -data GhcModError = GMENoMsg - -- ^ Unknown error - | GMEString String - -- ^ Some Error with a message. These are produced mostly by - -- 'fail' calls on GhcModT. - | GMEIOException IOException - -- ^ IOExceptions captured by GhcModT's MonadIO instance - | GMECabalConfigure GhcModError - -- ^ Configuring a cabal project failed. - | GMECabalFlags GhcModError - -- ^ Retrieval of the cabal configuration flags failed. - | GMEProcess [String] GhcModError - -- ^ Launching an operating system process failed. The first - -- field is the command. - | GMENoCabalFile - -- ^ No cabal file found. - | GMETooManyCabalFiles [FilePath] - -- ^ Too many cabal files found. - deriving (Eq,Show,Typeable) +import Exception +import Panic +import Config (cProjectVersion, cHostPlatformString) +import Paths_ghc_mod (version) -instance Exception GhcModError +import Language.Haskell.GhcMod.Types +import Language.Haskell.GhcMod.Pretty -instance Error GhcModError where - noMsg = GMENoMsg - strMsg = GMEString +type GmError m = MonadError GhcModError m + +gmCsfeDoc :: GMConfigStateFileError -> Doc +gmCsfeDoc GMConfigStateFileNoHeader = text $ + "Saved package config file header is missing. " + ++ "Try re-running the 'configure' command." + +gmCsfeDoc GMConfigStateFileBadHeader = text $ + "Saved package config file header is corrupt. " + ++ "Try re-running the 'configure' command." + +gmCsfeDoc GMConfigStateFileNoParse = text $ + "Saved package config file body is corrupt. " + ++ "Try re-running the 'configure' command." + +gmCsfeDoc GMConfigStateFileMissing = text $ + "Run the 'configure' command first." + +-- gmCsfeDoc (ConfigStateFileBadVersion oldCabal oldCompiler _) = text $ +-- "You need to re-run the 'configure' command. " +-- ++ "The version of Cabal being used has changed (was " +-- ++ display oldCabal ++ ", now " +-- ++ display currentCabalId ++ ")." +-- ++ badCompiler +-- where +-- badCompiler +-- | oldCompiler == currentCompilerId = "" +-- | otherwise = +-- " Additionally the compiler is different (was " +-- ++ display oldCompiler ++ ", now " +-- ++ display currentCompilerId +-- ++ ") which is probably the cause of the problem." gmeDoc :: GhcModError -> Doc gmeDoc e = case e of @@ -47,20 +90,83 @@ gmeDoc e = case e of text "Unknown error" GMEString msg -> text msg - GMEIOException ioe -> - text $ show ioe GMECabalConfigure msg -> - text "cabal configure failed: " <> gmeDoc msg + text "Configuring cabal project failed: " <> gmeDoc msg GMECabalFlags msg -> - text "retrieval of the cabal configuration flags failed: " <> gmeDoc msg - GMEProcess cmd msg -> - text ("launching operating system process `"++unwords cmd++"` failed: ") - <> gmeDoc msg + text "Retrieval of the cabal configuration flags failed: " <> gmeDoc msg + GMECabalComponent cn -> + text "Cabal component " <> quotes (gmComponentNameDoc cn) + <> text " could not be found." + GMECabalCompAssignment ctx -> + text "Could not find a consistent component assignment for modules:" $$ + (nest 4 $ foldr ($+$) empty $ map ctxDoc ctx) $$ + text "" $$ + (if all (Set.null . snd) ctx + then noComponentSuggestions + else empty) $$ + text "- To find out which components ghc-mod knows about try:" $$ + nest 4 (backticks $ text "ghc-mod debug") + + where + noComponentSuggestions = + text "- Are some of these modules part of a test and or benchmark?\ + \ Try enabling them:" $$ + nest 4 (backticks $ text "cabal configure --enable-tests [--enable-benchmarks]") + + backticks d = char '`' <> d <> char '`' + ctxDoc = moduleDoc *** compsDoc + >>> first (<> colon) >>> uncurry (flip hang 4) + + moduleDoc (Left fn) = + text "File " <> quotes (text fn) + moduleDoc (Right mdl) = + text "Module " <> quotes (text $ moduleNameString mdl) + + compsDoc sc | Set.null sc = text "has no known components" + compsDoc sc = fsep $ punctuate comma $ + map gmComponentNameDoc $ Set.toList sc + + GMEProcess cmd args emsg -> let c = showCommandForUser cmd args in + case emsg of + Right err -> + text (printf "Launching system command `%s` failed: " c) + <> gmeDoc err + Left (_out, _err, rv) -> text $ + printf "Launching system command `%s` failed (exited with %d)" c rv GMENoCabalFile -> text "No cabal file found." GMETooManyCabalFiles cfs -> text $ "Multiple cabal files found. Possible cabal files: \"" ++ intercalate "\", \"" cfs ++"\"." + GMECabalStateFile csfe -> + gmCsfeDoc csfe + +ghcExceptionDoc :: GhcException -> Doc +ghcExceptionDoc e@(CmdLineError _) = + text $ ": " ++ showGhcException e "" +ghcExceptionDoc (UsageError str) = strDoc str +ghcExceptionDoc (Panic msg) = vcat $ map text $ lines $ printf "\ +\GHC panic! (the 'impossible' happened)\n\ +\ ghc-mod version %s\n\ +\ GHC library version %s for %s:\n\ +\ %s\n\ +\\n\ +\Please report this as a bug: %s\n" + gmVer ghcVer platform msg url + where + gmVer = showVersion version + ghcVer = cProjectVersion + platform = cHostPlatformString + url = "https://github.com/kazu-yamamoto/ghc-mod/issues" :: String + +ghcExceptionDoc e = text $ showGhcException e "" + + +liftMaybe :: MonadError e m => e -> m (Maybe a) -> m a +liftMaybe e action = maybe (throwError e) return =<< action + +overrideError :: MonadError e m => e -> m a -> m a +overrideError e action = modifyError (const e) action modifyError :: MonadError e m => (e -> e) -> m a -> m a modifyError f action = action `catchError` \e -> throwError $ f e @@ -69,6 +175,24 @@ infixr 0 `modifyError'` modifyError' :: MonadError e m => m a -> (e -> e) -> m a modifyError' = flip modifyError + +modifyGmError :: (MonadIO m, ExceptionMonad m) + => (GhcModError -> GhcModError) -> m a -> m a +modifyGmError f a = gcatch a $ \(ex :: GhcModError) -> liftIO $ throwIO (f ex) + tryFix :: MonadError e m => m a -> (e -> m ()) -> m a -tryFix action fix = do - action `catchError` \e -> fix e >> action +tryFix action f = do + action `catchError` \e -> f e >> action + +data GHandler m a = forall e . Exception e => GHandler (e -> m a) + +gcatches :: (MonadIO m, ExceptionMonad m) => m a -> [GHandler m a] -> m a +gcatches io handlers = io `gcatch` gcatchesHandler handlers + +gcatchesHandler :: (MonadIO m, ExceptionMonad m) + => [GHandler m a] -> SomeException -> m a +gcatchesHandler handlers e = foldr tryHandler (liftIO $ throw e) handlers + where tryHandler (GHandler handler) res + = case fromException e of + Just e' -> handler e' + Nothing -> res diff --git a/Language/Haskell/GhcMod/FillSig.hs b/Language/Haskell/GhcMod/FillSig.hs index ae7e0ea..1f65f93 100644 --- a/Language/Haskell/GhcMod/FillSig.hs +++ b/Language/Haskell/GhcMod/FillSig.hs @@ -11,7 +11,8 @@ import Data.Char (isSymbol) import Data.Function (on) import Data.List (find, nub, sortBy) import qualified Data.Map as M -import Data.Maybe (isJust, catMaybes) +import Data.Maybe (catMaybes) +import Text.PrettyPrint (($$), text, nest) import Exception (ghandle, SomeException(..)) import GHC (GhcMonad, Id, ParsedModule(..), TypecheckedModule(..), DynFlags, SrcSpan, Type, GenLocated(L)) @@ -19,8 +20,12 @@ import qualified GHC as G import qualified Name as G import qualified Language.Haskell.GhcMod.Gap as Gap import Language.Haskell.GhcMod.Convert +import Language.Haskell.GhcMod.DynFlags import Language.Haskell.GhcMod.Monad import Language.Haskell.GhcMod.SrcUtils +import Language.Haskell.GhcMod.Logging (gmLog) +import Language.Haskell.GhcMod.Pretty (showDoc) +import Language.Haskell.GhcMod.Doc import Language.Haskell.GhcMod.Types import Outputable (PprStyle) import qualified Type as Ty @@ -31,6 +36,10 @@ import qualified HsPat as Ty import qualified Language.Haskell.Exts.Annotated as HE import Djinn.GHC +#if __GLASGOW_HASKELL__ >= 710 +import GHC (unLoc) +#endif + ---------------------------------------------------------------- -- INTIAL CODE FROM FUNCTION OR INSTANCE SIGNATURE ---------------------------------------------------------------- @@ -62,22 +71,27 @@ sig :: IOish m -> Int -- ^ Line number. -> Int -- ^ Column number. -> GhcModT m String -sig file lineNo colNo = ghandle handler body - where - body = inModuleContext file $ \dflag style -> do - opt <- options - modSum <- Gap.fileModSummary file - whenFound opt (getSignature modSum lineNo colNo) $ \s -> case s of +sig file lineNo colNo = + runGmlT' [Left file] deferErrors $ ghandle fallback $ do + opt <- options + style <- getStyle + dflag <- G.getSessionDynFlags + modSum <- Gap.fileModSummary file + whenFound opt (getSignature modSum lineNo colNo) $ \s -> + case s of Signature loc names ty -> - ("function", fourInts loc, map (initialBody dflag style ty) names) + ("function", fourInts loc, map (initialBody dflag style ty) names) + InstanceDecl loc cls -> - ("instance", fourInts loc, map (\x -> initialBody dflag style (G.idType x) x) - (Ty.classMethods cls)) + let body x = initialBody dflag style (G.idType x) x + in ("instance", fourInts loc, body `map` Ty.classMethods cls) + TyFamDecl loc name flavour vars -> let (rTy, initial) = initialTyFamString flavour - in (rTy, fourInts loc, [initial ++ initialFamBody dflag style name vars]) - - handler (SomeException _) = do + body = initialFamBody dflag style name vars + in (rTy, fourInts loc, [initial ++ body]) + where + fallback (SomeException _) = do opt <- options -- Code cannot be parsed by ghc module -- Fallback: try to get information via haskell-src-exts @@ -97,7 +111,11 @@ getSignature modSum lineNo colNo = do p@ParsedModule{pm_parsed_source = ps} <- G.parseModule modSum -- Inspect the parse tree to find the signature case listifyParsedSpans ps (lineNo, colNo) :: [G.LHsDecl G.RdrName] of +#if __GLASGOW_HASKELL__ >= 710 + [L loc (G.SigD (Ty.TypeSig names (L _ ty) _))] -> +#else [L loc (G.SigD (Ty.TypeSig names (L _ ty)))] -> +#endif -- We found a type signature return $ Just $ Signature loc (map G.unLoc names) ty [L _ (G.InstD _)] -> do @@ -125,7 +143,12 @@ getSignature modSum lineNo colNo = do G.TypeFamily -> Open G.DataFamily -> Data #endif -#if __GLASGOW_HASKELL__ >= 706 + +#if __GLASGOW_HASKELL__ >= 710 + getTyFamVarName x = case x of + L _ (G.UserTyVar n) -> n + L _ (G.KindedTyVar (G.L _ n) _) -> n +#elif __GLASGOW_HASKELL__ >= 706 getTyFamVarName x = case x of L _ (G.UserTyVar n) -> n L _ (G.KindedTyVar n _) -> n @@ -144,7 +167,8 @@ getSignature modSum lineNo colNo = do return $ InstanceDecl loc cls -- Get signature from haskell-src-exts -getSignatureFromHE :: GhcMonad m => FilePath -> Int -> Int -> m (Maybe HESigInfo) +getSignatureFromHE :: (MonadIO m, GhcMonad m) => + FilePath -> Int -> Int -> m (Maybe HESigInfo) getSignatureFromHE file lineNo colNo = do presult <- liftIO $ HE.parseFile file return $ case presult of @@ -220,9 +244,11 @@ initialHead1 :: String -> [FnArg] -> [String] -> String initialHead1 fname args elts = case initialBodyArgs1 args elts of [] -> fname - arglist -> if isSymbolName fname - then head arglist ++ " " ++ fname ++ " " ++ unwords (tail arglist) - else fname ++ " " ++ unwords arglist + arglist + | isSymbolName fname -> + head arglist ++ " " ++ fname ++ " " ++ unwords (tail arglist) + | otherwise -> + fname ++ " " ++ unwords arglist initialBodyArgs1 :: [FnArg] -> [String] -> [String] initialBodyArgs1 args elts = take (length args) elts @@ -238,12 +264,24 @@ class FnArgsInfo ty name | ty -> name, name -> ty where instance FnArgsInfo (G.HsType G.RdrName) (G.RdrName) where getFnName dflag style name = showOccName dflag style $ Gap.occName name - getFnArgs (G.HsForAllTy _ _ _ (L _ iTy)) = getFnArgs iTy +#if __GLASGOW_HASKELL__ >= 710 + getFnArgs (G.HsForAllTy _ _ _ _ (L _ iTy)) +#else + getFnArgs (G.HsForAllTy _ _ _ (L _ iTy)) +#endif + = getFnArgs iTy + getFnArgs (G.HsParTy (L _ iTy)) = getFnArgs iTy getFnArgs (G.HsFunTy (L _ lTy) (L _ rTy)) = (if fnarg lTy then FnArgFunction else FnArgNormal):getFnArgs rTy where fnarg ty = case ty of - (G.HsForAllTy _ _ _ (L _ iTy)) -> fnarg iTy +#if __GLASGOW_HASKELL__ >= 710 + (G.HsForAllTy _ _ _ _ (L _ iTy)) -> +#else + (G.HsForAllTy _ _ _ (L _ iTy)) -> +#endif + fnarg iTy + (G.HsParTy (L _ iTy)) -> fnarg iTy (G.HsFunTy _ _) -> True _ -> False @@ -301,48 +339,61 @@ refine :: IOish m -> Int -- ^ Column number. -> Expression -- ^ A Haskell expression. -> GhcModT m String -refine file lineNo colNo expr = ghandle handler body +refine file lineNo colNo (Expression expr) = + ghandle handler $ + runGmlT' [Left file] deferErrors $ do + opt <- options + style <- getStyle + dflag <- G.getSessionDynFlags + modSum <- Gap.fileModSummary file + p <- G.parseModule modSum + tcm@TypecheckedModule{tm_typechecked_source = tcs} <- G.typecheckModule p + ety <- G.exprType expr + whenFound opt (findVar dflag style tcm tcs lineNo colNo) $ + \(loc, name, rty, paren) -> + let eArgs = getFnArgs ety + rArgs = getFnArgs rty + diffArgs' = length eArgs - length rArgs + diffArgs = if diffArgs' < 0 then 0 else diffArgs' + iArgs = take diffArgs eArgs + txt = initialHead1 expr iArgs (infinitePrefixSupply name) + in (fourInts loc, doParen paren txt) where - body = inModuleContext file $ \dflag style -> do - opt <- options - modSum <- Gap.fileModSummary file - p <- G.parseModule modSum - tcm@TypecheckedModule{tm_typechecked_source = tcs} <- G.typecheckModule p - ety <- G.exprType expr - whenFound opt (findVar dflag style tcm tcs lineNo colNo) $ - \(loc, name, rty, paren) -> - let eArgs = getFnArgs ety - rArgs = getFnArgs rty - diffArgs' = length eArgs - length rArgs - diffArgs = if diffArgs' < 0 then 0 else diffArgs' - iArgs = take diffArgs eArgs - text = initialHead1 expr iArgs (infinitePrefixSupply name) - in (fourInts loc, doParen paren text) - - handler (SomeException _) = emptyResult =<< options + handler (SomeException ex) = do + gmLog GmDebug "refining" $ + text "" $$ nest 4 (showDoc ex) + emptyResult =<< options -- Look for the variable in the specified position -findVar :: GhcMonad m => DynFlags -> PprStyle - -> G.TypecheckedModule -> G.TypecheckedSource - -> Int -> Int -> m (Maybe (SrcSpan, String, Type, Bool)) +findVar + :: GhcMonad m + => DynFlags + -> PprStyle + -> G.TypecheckedModule + -> G.TypecheckedSource + -> Int + -> Int + -> m (Maybe (SrcSpan, String, Type, Bool)) findVar dflag style tcm tcs lineNo colNo = - let lst = sortBy (cmp `on` G.getLoc) $ - listifySpans tcs (lineNo, colNo) :: [G.LHsExpr Id] - in case lst of - e@(L _ (G.HsVar i)):others -> - do tyInfo <- Gap.getType tcm e - let name = getFnName dflag style i - if (name == "undefined" || head name == '_') && isJust tyInfo - then let Just (s,t) = tyInfo - b = case others of -- If inside an App, we need - -- parenthesis - [] -> False - L _ (G.HsApp (L _ a1) (L _ a2)):_ -> - isSearchedVar i a1 || isSearchedVar i a2 - _ -> False - in return $ Just (s, name, t, b) - else return Nothing - _ -> return Nothing + case lst of + e@(L _ (G.HsVar i)):others -> do + tyInfo <- Gap.getType tcm e + case tyInfo of + Just (s, typ) + | name == "undefined" || head name == '_' -> + return $ Just (s, name, typ, b) + where + name = getFnName dflag style i + -- If inside an App, we need parenthesis + b = case others of + L _ (G.HsApp (L _ a1) (L _ a2)):_ -> + isSearchedVar i a1 || isSearchedVar i a2 + _ -> False + _ -> return Nothing + _ -> return Nothing + where + lst :: [G.LHsExpr Id] + lst = sortBy (cmp `on` G.getLoc) $ listifySpans tcs (lineNo, colNo) infinitePrefixSupply :: String -> [String] infinitePrefixSupply "undefined" = repeat "undefined" @@ -366,10 +417,11 @@ auto :: IOish m -> Int -- ^ Line number. -> Int -- ^ Column number. -> GhcModT m String -auto file lineNo colNo = ghandle handler body - where - body = inModuleContext file $ \dflag style -> do +auto file lineNo colNo = + ghandle handler $ runGmlT' [Left file] deferErrors $ do opt <- options + style <- getStyle + dflag <- G.getSessionDynFlags modSum <- Gap.fileModSummary file p <- G.parseModule modSum tcm@TypecheckedModule { @@ -395,8 +447,11 @@ auto file lineNo colNo = ghandle handler body djinns <- djinn True (Just minfo) env rty (Max 10) 100000 return ( fourInts loc , map (doParen paren) $ nub (djinnsEmpty ++ djinns)) - - handler (SomeException _) = emptyResult =<< options + where + handler (SomeException ex) = do + gmLog GmDebug "auto-refining" $ + text "" $$ nest 4 (showDoc ex) + emptyResult =<< options -- Functions we do not want in completions notWantedFuns :: [String] @@ -443,7 +498,11 @@ getPatsForVariable tcs (lineNo, colNo) = #else :: [G.LMatch Id] #endif +#if __GLASGOW_HASKELL__ >= 710 + (L _ (G.Match _ pats _ _):_) = m +#else (L _ (G.Match pats _ _):_) = m +#endif in (funId, pats) _ -> (error "This should never happen", []) @@ -478,7 +537,13 @@ getBindingsForRecPat (Ty.PrefixCon args) = getBindingsForRecPat (Ty.InfixCon (L _ a1) (L _ a2)) = M.union (getBindingsForPat a1) (getBindingsForPat a2) getBindingsForRecPat (Ty.RecCon (Ty.HsRecFields { Ty.rec_flds = fields })) = - getBindingsForRecFields fields - where getBindingsForRecFields [] = M.empty - getBindingsForRecFields (Ty.HsRecField {Ty.hsRecFieldArg = (L _ a)}:fs) = - M.union (getBindingsForPat a) (getBindingsForRecFields fs) + getBindingsForRecFields (map unLoc' fields) + where +#if __GLASGOW_HASKELL__ >= 710 + unLoc' = unLoc +#else + unLoc' = id +#endif + getBindingsForRecFields [] = M.empty + getBindingsForRecFields (Ty.HsRecField {Ty.hsRecFieldArg = (L _ a)}:fs) = + M.union (getBindingsForPat a) (getBindingsForRecFields fs) diff --git a/Language/Haskell/GhcMod/Find.hs b/Language/Haskell/GhcMod/Find.hs index 37ba7fe..b001e0d 100644 --- a/Language/Haskell/GhcMod/Find.hs +++ b/Language/Haskell/GhcMod/Find.hs @@ -1,9 +1,8 @@ -{-# LANGUAGE CPP, BangPatterns #-} +{-# LANGUAGE CPP, BangPatterns, DoAndIfThenElse #-} module Language.Haskell.GhcMod.Find #ifndef SPEC - ( - Symbol + ( Symbol , SymbolDb , loadSymbolDb , lookupSymbol @@ -15,65 +14,51 @@ module Language.Haskell.GhcMod.Find #endif where -import Control.Applicative ((<$>)) +import Control.Applicative import Control.Monad (when, void) import Data.Function (on) import Data.List (groupBy, sort) -import Data.Maybe (fromMaybe) import qualified GHC as G import Language.Haskell.GhcMod.Convert +import Language.Haskell.GhcMod.Gap (listVisibleModules) import Language.Haskell.GhcMod.Monad +import Language.Haskell.GhcMod.PathsAndFiles import Language.Haskell.GhcMod.Types import Language.Haskell.GhcMod.Utils -import Language.Haskell.GhcMod.PathsAndFiles +import Language.Haskell.GhcMod.World (timedPackageCaches) +import Language.Haskell.GhcMod.Output import Name (getOccString) +import Module (moduleName) import System.Directory (doesFileExist, getModificationTime) -import System.FilePath ((), takeDirectory) +import System.FilePath (()) import System.IO +import Prelude -#ifndef MIN_VERSION_containers -#define MIN_VERSION_containers(x,y,z) 1 -#endif - -#if MIN_VERSION_containers(0,5,0) -import Data.Map.Strict (Map) -import qualified Data.Map.Strict as M -#else import Data.Map (Map) import qualified Data.Map as M -#endif ---------------------------------------------------------------- -- | Type of function and operation names. type Symbol = String -- | Database from 'Symbol' to \['ModuleString'\]. -data SymbolDb = SymbolDb { - table :: Map Symbol [ModuleString] - , packageCachePath :: FilePath +data SymbolDb = SymbolDb + { table :: Map Symbol [ModuleString] , symbolDbCachePath :: FilePath } deriving (Show) -isOutdated :: SymbolDb -> IO Bool -isOutdated db = symbolDbCachePath db `isOlderThan` packageCachePath db - ----------------------------------------------------------------- - --- | When introducing incompatible changes to the 'symbolCache' file format --- increment this version number. -symbolCacheVersion :: Integer -symbolCacheVersion = 0 - --- | Filename of the symbol table cache file. -symbolCache :: String -symbolCache = "ghc-mod-"++ show symbolCacheVersion ++".cache" +isOutdated :: IOish m => SymbolDb -> GhcModT m Bool +isOutdated db = + (liftIO . isOlderThan (symbolDbCachePath db)) =<< timedPackageCaches ---------------------------------------------------------------- -- | Looking up 'SymbolDb' with 'Symbol' to \['ModuleString'\] -- which will be concatenated. 'loadSymbolDb' is called internally. findSymbol :: IOish m => Symbol -> GhcModT m String -findSymbol sym = loadSymbolDb >>= lookupSymbol sym +findSymbol sym = do + tmpdir <- cradleTempDir <$> cradle + loadSymbolDb tmpdir >>= lookupSymbol sym -- | Looking up 'SymbolDb' with 'Symbol' to \['ModuleString'\] -- which will be concatenated. @@ -81,25 +66,25 @@ lookupSymbol :: IOish m => Symbol -> SymbolDb -> GhcModT m String lookupSymbol sym db = convert' $ lookupSym sym db lookupSym :: Symbol -> SymbolDb -> [ModuleString] -lookupSym sym db = fromMaybe [] $ M.lookup sym $ table db +lookupSym sym db = M.findWithDefault [] sym $ table db --------------------------------------------------------------- -- | Loading a file and creates 'SymbolDb'. -loadSymbolDb :: IOish m => GhcModT m SymbolDb -loadSymbolDb = do - ghcMod <- liftIO ghcModExecutable - tmpdir <- cradleTempDir <$> cradle - file <- chop <$> readProcess' ghcMod ["dumpsym", tmpdir] - !db <- M.fromAscList . map conv . lines <$> liftIO (readFile file) - return $ SymbolDb { - table = db - , packageCachePath = takeDirectory file packageCache - , symbolDbCachePath = file - } +loadSymbolDb :: IOish m => FilePath -> GhcModT m SymbolDb +loadSymbolDb dir = do + ghcMod <- liftIO ghcModExecutable + readProc <- gmReadProcess + file <- liftIO $ chop <$> readProc ghcMod ["dumpsym", dir] "" + !db <- M.fromAscList . map conv . lines <$> liftIO (readFile file) + return $ SymbolDb + { table = db + , symbolDbCachePath = file + } where - conv :: String -> (Symbol,[ModuleString]) + conv :: String -> (Symbol, [ModuleString]) conv = read + chop :: String -> String chop "" = "" chop xs = init xs @@ -112,54 +97,52 @@ loadSymbolDb = do dumpSymbol :: IOish m => FilePath -> GhcModT m String dumpSymbol dir = do - let cache = dir symbolCache - pkgdb = dir packageCache - - create <- liftIO $ cache `isOlderThan` pkgdb - when create $ (liftIO . writeSymbolCache cache) =<< getSymbolTable + create <- (liftIO . isOlderThan cache) =<< timedPackageCaches + runGmPkgGhc $ do + when create $ + liftIO . writeSymbolCache cache =<< getGlobalSymbolTable return $ unlines [cache] + where + cache = dir symbolCacheFile writeSymbolCache :: FilePath - -> [(Symbol,[ModuleString])] + -> [(Symbol, [ModuleString])] -> IO () writeSymbolCache cache sm = void . withFile cache WriteMode $ \hdl -> - mapM (hPrint hdl) sm + mapM (hPrint hdl) sm -isOlderThan :: FilePath -> FilePath -> IO Bool -isOlderThan cache file = do - exist <- doesFileExist cache - if not exist then - return True - else do - tCache <- getModificationTime cache - tFile <- getModificationTime file - return $ tCache <= tFile -- including equal just in case +-- | Check whether given file is older than any file from the given set. +-- Returns True if given file does not exist. +isOlderThan :: FilePath -> [TimedFile] -> IO Bool +isOlderThan cache files = do + exist <- doesFileExist cache + if not exist + then return True + else do + tCache <- getModificationTime cache + return $ any (tCache <=) $ map tfTime files -- including equal just in case --- | Browsing all functions in all system/user modules. -getSymbolTable :: IOish m => GhcModT m [(Symbol,[ModuleString])] -getSymbolTable = do - ghcModules <- G.packageDbModules True - moduleInfos <- mapM G.getModuleInfo ghcModules - let modules = do - m <- ghcModules - let moduleName = G.moduleNameString $ G.moduleName m --- modulePkg = G.packageIdString $ G.modulePackageId m - return moduleName +-- | Browsing all functions in all system modules. +getGlobalSymbolTable :: LightGhc [(Symbol, [ModuleString])] +getGlobalSymbolTable = do + df <- G.getSessionDynFlags + let mods = listVisibleModules df + moduleInfos <- mapM G.getModuleInfo mods + return $ collectModules + $ extractBindings `concatMap` (moduleInfos `zip` mods) - return $ collectModules - $ extractBindings `concatMap` (moduleInfos `zip` modules) - -extractBindings :: (Maybe G.ModuleInfo, ModuleString) +extractBindings :: (Maybe G.ModuleInfo, G.Module) -> [(Symbol, ModuleString)] -extractBindings (Nothing,_) = [] -extractBindings (Just inf,mdlname) = - map (\name -> (getOccString name, mdlname)) names +extractBindings (Nothing, _) = [] +extractBindings (Just inf, mdl) = + map (\name -> (getOccString name, modStr)) names where - names = G.modInfoExports inf + names = G.modInfoExports inf + modStr = ModuleString $ moduleNameString $ moduleName mdl -collectModules :: [(Symbol,ModuleString)] - -> [(Symbol,[ModuleString])] +collectModules :: [(Symbol, ModuleString)] + -> [(Symbol, [ModuleString])] collectModules = map tieup . groupBy ((==) `on` fst) . sort where tieup x = (head (map fst x), map snd x) diff --git a/Language/Haskell/GhcMod/GHCApi.hs b/Language/Haskell/GhcMod/GHCApi.hs deleted file mode 100644 index 0f10545..0000000 --- a/Language/Haskell/GhcMod/GHCApi.hs +++ /dev/null @@ -1,86 +0,0 @@ -{-# LANGUAGE ScopedTypeVariables #-} - -module Language.Haskell.GhcMod.GHCApi ( - ghcPkgDb - , package - , modules - , findModule - , moduleInfo - , localModuleInfo - , bindings - ) where - -import Language.Haskell.GhcMod.GhcPkg -import Language.Haskell.GhcMod.Monad (GhcModT) -import Language.Haskell.GhcMod.Target (setTargetFiles) -import Language.Haskell.GhcMod.Types - -import Control.Applicative ((<$>)) -import Distribution.Package (InstalledPackageId(..)) -import qualified Data.Map as M -import GHC (DynFlags(..)) -import qualified GHC as G -import GhcMonad -import qualified Packages as G -import qualified Module as G -import qualified OccName as G - ----------------------------------------------------------------- --- get Packages,Modules,Bindings - -ghcPkgDb :: GhcMonad m => m PkgDb -ghcPkgDb = M.fromList <$> - maybe [] (map toKv . filterInternal) <$> pkgDatabase <$> G.getSessionDynFlags - where - toKv pkg = (fromInstalledPackageId $ G.installedPackageId pkg, pkg) - filterInternal = - filter ((/= InstalledPackageId "builtin_rts") . G.installedPackageId) - -package :: G.PackageConfig -> Package -package = fromInstalledPackageId . G.installedPackageId - -modules :: G.PackageConfig -> [ModuleString] -modules = map G.moduleNameString . G.exposedModules - -findModule :: ModuleString -> PkgDb -> [Package] -findModule m db = M.elems $ package `M.map` (containsModule `M.filter` db) - where - containsModule :: G.PackageConfig -> Bool - containsModule pkgConf = - G.mkModuleName m `elem` G.exposedModules pkgConf - - -ghcPkgId :: Package -> G.PackageId -ghcPkgId (name,_,_) = - -- TODO: Adding the package version too breaks 'findModule' for some reason - -- this isn't a big deal since in the common case where we're in a cabal - -- project we just use cabal's view of package dependencies anyways so we're - -- guaranteed to only have one version of each package exposed. However when - -- we're operating without a cabal project this will probaly cause trouble. - G.stringToPackageId name - -type Binding = String - --- | @moduleInfo mpkg module@. @mpkg@ should be 'Nothing' iff. moduleInfo --- should look for @module@ in the working directory. --- --- To map a 'ModuleString' to a package see 'findModule' -moduleInfo :: IOish m - => Maybe Package - -> ModuleString - -> GhcModT m (Maybe G.ModuleInfo) -moduleInfo mpkg mdl = do - let mdlName = G.mkModuleName mdl - mfsPkgId = G.packageIdFS . ghcPkgId <$> mpkg - loadLocalModule - G.findModule mdlName mfsPkgId >>= G.getModuleInfo - where - loadLocalModule = case mpkg of - Just _ -> return () - Nothing -> setTargetFiles [mdl] - -localModuleInfo :: IOish m => ModuleString -> GhcModT m (Maybe G.ModuleInfo) -localModuleInfo mdl = moduleInfo Nothing mdl - -bindings :: G.ModuleInfo -> [Binding] -bindings minfo = map (G.occNameString . G.getOccName) $ G.modInfoExports minfo diff --git a/Language/Haskell/GhcMod/GHCChoice.hs b/Language/Haskell/GhcMod/GHCChoice.hs deleted file mode 100644 index 8ceb214..0000000 --- a/Language/Haskell/GhcMod/GHCChoice.hs +++ /dev/null @@ -1,23 +0,0 @@ -{-# LANGUAGE ScopedTypeVariables #-} - -module Language.Haskell.GhcMod.GHCChoice where - -import Control.Exception (IOException) -import CoreMonad (liftIO) -import qualified Exception as GE -import GHC (GhcMonad) - ----------------------------------------------------------------- - --- | Try the left 'Ghc' action. If 'IOException' occurs, try --- the right 'Ghc' action. -(||>) :: GhcMonad m => m a -> m a -> m a -x ||> y = x `GE.gcatch` (\(_ :: IOException) -> y) - --- | Go to the next 'Ghc' monad by throwing 'AltGhcgoNext'. -goNext :: GhcMonad m => m a -goNext = liftIO . GE.throwIO $ userError "goNext" - --- | Run any one 'Ghc' monad. -runAnyOne :: GhcMonad m => [m a] -> m a -runAnyOne = foldr (||>) goNext diff --git a/Language/Haskell/GhcMod/Gap.hs b/Language/Haskell/GhcMod/Gap.hs index fbbadd0..c8b6e0f 100644 --- a/Language/Haskell/GhcMod/Gap.hs +++ b/Language/Haskell/GhcMod/Gap.hs @@ -13,7 +13,6 @@ module Language.Haskell.GhcMod.Gap ( , showSeverityCaption , setCabalPkg , setHideAllPackages - , addPackageFlags , setDeferTypeErrors , setWarnTypedHoles , setDumpSplices @@ -33,14 +32,15 @@ module Language.Haskell.GhcMod.Gap ( , fileModSummary , WarnFlags , emptyWarnFlags - , benchmarkBuildInfo - , benchmarkTargets - , toModuleString , GLMatch , GLMatchI , getClass , occName - , setFlags + , listVisibleModuleNames + , listVisibleModules + , lookupModulePackageInAllPackages + , Language.Haskell.GhcMod.Gap.isSynTyCon + , parseModuleHeader ) where import Control.Applicative hiding (empty) @@ -49,15 +49,15 @@ import CoreSyn (CoreExpr) import Data.List (intersperse) import Data.Maybe (catMaybes) import Data.Time.Clock (UTCTime) +import Data.Traversable hiding (mapM) import DataCon (dataConRepType) import Desugar (deSugarExpr) import DynFlags import ErrUtils +import Exception import FastString import GhcMonad import HscTypes -import Language.Haskell.GhcMod.GHCChoice -import Language.Haskell.GhcMod.Types import NameSet import OccName import Outputable @@ -65,8 +65,8 @@ import PprTyThing import StringBuffer import TcType import Var (varType) +import System.Directory -import qualified Distribution.PackageDescription as P import qualified InstEnv import qualified Pretty import qualified StringBuffer as SB @@ -88,11 +88,24 @@ import Data.Convertible import RdrName (rdrNameOcc) #endif +#if __GLASGOW_HASKELL__ < 710 +import UniqFM (eltsUFM) +import Module +#endif + #if __GLASGOW_HASKELL__ >= 704 import qualified Data.IntSet as I (IntSet, empty) -import qualified Distribution.ModuleName as M (ModuleName,toFilePath) #endif +import Bag +import Lexer as L +import Parser +import SrcLoc +import Packages + +import Language.Haskell.GhcMod.Types (Expression(..)) +import Prelude + ---------------------------------------------------------------- ---------------------------------------------------------------- -- @@ -173,7 +186,11 @@ toStringBuffer = liftIO . stringToStringBuffer . unlines ---------------------------------------------------------------- fOptions :: [String] -#if __GLASGOW_HASKELL__ >= 704 +#if __GLASGOW_HASKELL__ >= 710 +fOptions = [option | (FlagSpec option _ _ _) <- fFlags] + ++ [option | (FlagSpec option _ _ _) <- fWarningFlags] + ++ [option | (FlagSpec option _ _ _) <- fLangFlags] +#elif __GLASGOW_HASKELL__ >= 704 fOptions = [option | (option,_,_) <- fFlags] ++ [option | (option,_,_) <- fWarningFlags] ++ [option | (option,_,_) <- fLangFlags] @@ -187,9 +204,11 @@ fOptions = [option | (option,_,_,_) <- fFlags] ---------------------------------------------------------------- fileModSummary :: GhcMonad m => FilePath -> m ModSummary -fileModSummary file = do +fileModSummary file' = do mss <- getModuleGraph - let [ms] = filter (\m -> ml_hs_file (ms_location m) == Just file) mss + file <- liftIO $ canonicalizePath file' + [ms] <- liftIO $ flip filterM mss $ \m -> + (Just file==) <$> canonicalizePath `traverse` ml_hs_file (ms_location m) return ms withContext :: GhcMonad m => m a -> m a @@ -202,26 +221,31 @@ withContext action = gbracket setup teardown body action topImports = do mss <- getModuleGraph - ms <- map modName <$> filterM isTop mss + mns <- map modName <$> filterM isTop mss + let ii = map IIModule mns #if __GLASGOW_HASKELL__ >= 704 - return ms + return ii #else - return (ms,[]) + return (ii,[]) #endif isTop mos = lookupMod mos ||> returnFalse lookupMod mos = lookupModule (ms_mod_name mos) Nothing >> return True returnFalse = return False #if __GLASGOW_HASKELL__ >= 706 - modName = IIModule . moduleName . ms_mod + modName = moduleName . ms_mod setCtx = setContext #elif __GLASGOW_HASKELL__ >= 704 - modName = IIModule . ms_mod + modName = ms_mod setCtx = setContext #else modName = ms_mod setCtx = uncurry setContext #endif +-- | Try the left action, if an IOException occurs try the right action. +(||>) :: ExceptionMonad m => m a -> m a -> m a +x ||> y = x `gcatch` (\(_ :: IOException) -> y) + showSeverityCaption :: Severity -> String #if __GLASGOW_HASKELL__ >= 706 showSeverityCaption SevWarning = "Warning: " @@ -249,12 +273,6 @@ setHideAllPackages df = gopt_set df Opt_HideAllPackages setHideAllPackages df = dopt_set df Opt_HideAllPackages #endif -addPackageFlags :: [Package] -> DynFlags -> DynFlags -addPackageFlags pkgs df = - df { packageFlags = packageFlags df ++ expose `map` pkgs } - where - expose pkg = ExposePackageId $ showPkgId pkg - ---------------------------------------------------------------- setDumpSplices :: DynFlags -> DynFlags @@ -310,8 +328,8 @@ filterOutChildren get_thing xs where implicits = mkNameSet [getName t | x <- xs, t <- implicitTyThings (get_thing x)] -infoThing :: GhcMonad m => String -> m SDoc -infoThing str = do +infoThing :: GhcMonad m => Expression -> m SDoc +infoThing (Expression str) = do names <- parseName str #if __GLASGOW_HASKELL__ >= 708 mb_stuffs <- mapM (getInfo False) names @@ -413,29 +431,6 @@ emptyWarnFlags = [] ---------------------------------------------------------------- ---------------------------------------------------------------- -benchmarkBuildInfo :: P.PackageDescription -> [P.BuildInfo] -#if __GLASGOW_HASKELL__ >= 704 -benchmarkBuildInfo pd = map P.benchmarkBuildInfo $ P.benchmarks pd -#else -benchmarkBuildInfo pd = [] -#endif - -benchmarkTargets :: P.PackageDescription -> [String] -#if __GLASGOW_HASKELL__ >= 704 -benchmarkTargets pd = map toModuleString $ concatMap P.benchmarkModules $ P.benchmarks pd -#else -benchmarkTargets = [] -#endif - -toModuleString :: M.ModuleName -> String -toModuleString mn = fromFilePath $ M.toFilePath mn - where - fromFilePath :: FilePath -> String - fromFilePath fp = map (\c -> if c=='/' then '.' else c) fp - ----------------------------------------------------------------- ----------------------------------------------------------------- - #if __GLASGOW_HASKELL__ >= 708 type GLMatch = LMatch RdrName (LHsExpr RdrName) type GLMatchI = LMatch Id (LHsExpr Id) @@ -445,7 +440,12 @@ type GLMatchI = LMatch Id #endif getClass :: [LInstDecl Name] -> Maybe (Name, SrcSpan) -#if __GLASGOW_HASKELL__ >= 708 +#if __GLASGOW_HASKELL__ >= 710 +-- Instance declarations of sort 'instance F (G a)' +getClass [L loc (ClsInstD (ClsInstDecl {cid_poly_ty = (L _ (HsForAllTy _ _ _ _ (L _ (HsAppTy (L _ (HsTyVar className)) _))))}))] = Just (className, loc) +-- Instance declarations of sort 'instance F G' (no variables) +getClass [L loc (ClsInstD (ClsInstDecl {cid_poly_ty = (L _ (HsAppTy (L _ (HsTyVar className)) _))}))] = Just (className, loc) +#elif __GLASGOW_HASKELL__ >= 708 -- Instance declarations of sort 'instance F (G a)' getClass [L loc (ClsInstD (ClsInstDecl {cid_poly_ty = (L _ (HsForAllTy _ _ _ (L _ (HsAppTy (L _ (HsTyVar className)) _))))}))] = Just (className, loc) -- Instance declarations of sort 'instance F G' (no variables) @@ -464,12 +464,74 @@ occName :: RdrName -> OccName occName = rdrNameOcc #endif ----------------------------------------------------------------- ---------------------------------------------------------------- -setFlags :: DynFlags -> DynFlags -#if __GLASGOW_HASKELL__ >= 708 -setFlags df = df `gopt_unset` Opt_SpecConstr -- consume memory if -O2 -#else -setFlags = id +#if __GLASGOW_HASKELL__ < 710 +-- Copied from ghc/InteractiveUI.hs +allExposedPackageConfigs :: DynFlags -> [PackageConfig] +allExposedPackageConfigs df = filter exposed $ eltsUFM $ pkgIdMap $ pkgState df + +allExposedModules :: DynFlags -> [ModuleName] +allExposedModules df = concat $ map exposedModules $ allExposedPackageConfigs df + +listVisibleModuleNames :: DynFlags -> [ModuleName] +listVisibleModuleNames = allExposedModules #endif + +lookupModulePackageInAllPackages :: + DynFlags -> ModuleName -> [String] +lookupModulePackageInAllPackages df mn = +#if __GLASGOW_HASKELL__ >= 710 + unpackSPId . sourcePackageId . snd <$> lookupModuleInAllPackages df mn + where + unpackSPId (SourcePackageId fs) = unpackFS fs +#else + unpackPId . sourcePackageId . fst <$> lookupModuleInAllPackages df mn + where + unpackPId pid = packageIdString $ mkPackageId pid +-- n ++ "-" ++ showVersion v +#endif + +listVisibleModules :: DynFlags -> [GHC.Module] +listVisibleModules df = let +#if __GLASGOW_HASKELL__ >= 710 + modNames = listVisibleModuleNames df + mods = [ m | mn <- modNames, (m, _) <- lookupModuleInAllPackages df mn ] +#else + pkgCfgs = allExposedPackageConfigs df + mods = [ mkModule pid modname | p <- pkgCfgs + , let pid = packageConfigId p + , modname <- exposedModules p ] +#endif + in mods + +isSynTyCon :: TyCon -> Bool +#if __GLASGOW_HASKELL__ >= 710 +isSynTyCon = GHC.isTypeSynonymTyCon +#else +isSynTyCon = GHC.isSynTyCon +#endif + + +parseModuleHeader + :: String -- ^ Haskell module source text (full Unicode is supported) + -> DynFlags + -> FilePath -- ^ the filename (for source locations) + -> Either ErrorMessages (WarningMessages, Located (HsModule RdrName)) +parseModuleHeader str dflags filename = + let + loc = mkRealSrcLoc (mkFastString filename) 1 1 + buf = stringToStringBuffer str + in + case L.unP Parser.parseHeader (mkPState dflags buf loc) of + + PFailed sp err -> +#if __GLASGOW_HASKELL__ >= 706 + Left (unitBag (mkPlainErrMsg dflags sp err)) +#else + Left (unitBag (mkPlainErrMsg sp err)) +#endif + + POk pst rdr_module -> + let (warns,_) = getMessages pst in + Right (warns, rdr_module) diff --git a/Language/Haskell/GhcMod/GhcPkg.hs b/Language/Haskell/GhcMod/GhcPkg.hs index 969acda..f6c281b 100644 --- a/Language/Haskell/GhcMod/GhcPkg.hs +++ b/Language/Haskell/GhcMod/GhcPkg.hs @@ -4,53 +4,27 @@ module Language.Haskell.GhcMod.GhcPkg ( , ghcPkgDbStackOpts , ghcDbStackOpts , ghcDbOpt - , fromInstalledPackageId - , fromInstalledPackageId' , getPackageDbStack , getPackageCachePaths ) where import Config (cProjectVersion, cTargetPlatformString, cProjectVersionInt) -import Control.Applicative ((<$>)) -import Data.List (intercalate) +import Control.Applicative import Data.List.Split (splitOn) import Data.Maybe -import Distribution.Package (InstalledPackageId(..)) import Exception (handleIO) -import Language.Haskell.GhcMod.PathsAndFiles -import Language.Haskell.GhcMod.Types import System.Directory (doesDirectoryExist, getAppUserDataDirectory) import System.FilePath (()) +import Prelude + +import Language.Haskell.GhcMod.Types +import Language.Haskell.GhcMod.Monad.Types +import Language.Haskell.GhcMod.CabalHelper +import Language.Haskell.GhcMod.PathsAndFiles ghcVersion :: Int ghcVersion = read cProjectVersionInt -getPackageDbStack :: FilePath -- ^ Project Directory (where the - -- cabal.sandbox.config file would be if it - -- exists) - -> IO [GhcPkgDb] -getPackageDbStack cdir = do - mSDir <- getSandboxDb cdir - return $ [GlobalDb] ++ case mSDir of - Nothing -> [UserDb] - Just db -> [PackageDb db] - ----------------------------------------------------------------- - -fromInstalledPackageId' :: InstalledPackageId -> Maybe Package -fromInstalledPackageId' pid = let - InstalledPackageId pkg = pid - in case reverse $ splitOn "-" pkg of - i:v:rest -> Just (intercalate "-" (reverse rest), v, i) - _ -> Nothing - -fromInstalledPackageId :: InstalledPackageId -> Package -fromInstalledPackageId pid = - case fromInstalledPackageId' pid of - Just p -> p - Nothing -> error $ - "fromInstalledPackageId: `"++show pid++"' is not a valid package-id" - ---------------------------------------------------------------- -- | Get options needed to add a list of package dbs to ghc-pkg's db stack @@ -85,11 +59,24 @@ ghcDbOpt (PackageDb pkgDb) ---------------------------------------------------------------- +getPackageDbStack :: IOish m => GhcModT m [GhcPkgDb] +getPackageDbStack = do + crdl <- cradle + mCusPkgStack <- getCustomPkgDbStack + stack <- case cradleProjectType crdl of + PlainProject -> + return [GlobalDb, UserDb] + SandboxProject -> do + Just db <- liftIO $ getSandboxDb $ cradleRootDir crdl + return $ [GlobalDb, db] + CabalProject -> + getCabalPackageDbStack + return $ fromMaybe stack mCusPkgStack -getPackageCachePaths :: FilePath -> Cradle -> IO [FilePath] -getPackageCachePaths sysPkgCfg crdl = - catMaybes <$> resolvePackageConfig sysPkgCfg `mapM` cradlePkgDbStack crdl - +getPackageCachePaths :: IOish m => FilePath -> GhcModT m [FilePath] +getPackageCachePaths sysPkgCfg = do + pkgDbStack <- getPackageDbStack + catMaybes <$> (liftIO . resolvePackageConfig sysPkgCfg) `mapM` pkgDbStack -- TODO: use PkgConfRef --- Copied from ghc module `Packages' unfortunately it's not exported :/ diff --git a/Language/Haskell/GhcMod/HomeModuleGraph.hs b/Language/Haskell/GhcMod/HomeModuleGraph.hs new file mode 100644 index 0000000..d10f483 --- /dev/null +++ b/Language/Haskell/GhcMod/HomeModuleGraph.hs @@ -0,0 +1,263 @@ +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +{-# LANGUAGE ScopedTypeVariables, RecordWildCards #-} +module Language.Haskell.GhcMod.HomeModuleGraph ( + GmModuleGraph(..) + , ModulePath(..) + , mkFileMap + , mkModuleMap + , mkMainModulePath + , findModulePath + , findModulePathSet + , fileModuleName + , canonicalizeModulePath + , homeModuleGraph + , updateHomeModuleGraph + , canonicalizeModuleGraph + , reachable + , moduleGraphToDot + ) where + +import DriverPipeline +import DynFlags +import ErrUtils +import Exception +import Finder +import GHC +import HscTypes + +import Control.Arrow ((&&&)) +import Control.Applicative +import Control.Monad +import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT) +import Control.Monad.State.Strict (execStateT) +import Control.Monad.State.Class +import Data.Maybe +import Data.Monoid as Monoid +import Data.Map (Map) +import qualified Data.Map as Map +import Data.Set (Set) +import qualified Data.Set as Set +import System.FilePath +import System.Directory +import Prelude + +import Language.Haskell.GhcMod.Logging +import Language.Haskell.GhcMod.Logger +import Language.Haskell.GhcMod.Monad.Types +import Language.Haskell.GhcMod.Types +import Language.Haskell.GhcMod.Gap (parseModuleHeader) + +-- | Turn module graph into a graphviz dot file +-- +-- @dot -Tpng -o modules.png modules.dot@ +moduleGraphToDot :: GmModuleGraph -> String +moduleGraphToDot GmModuleGraph { gmgGraph } = + "digraph {\n" ++ concatMap edges (Map.toList graph) ++ "}\n" + where + graph = Map.map (Set.mapMonotonic mpPath) + $ Map.mapKeysMonotonic mpPath gmgGraph + edges :: (FilePath, (Set FilePath)) -> String + edges (f, sf) = + concatMap (\f' -> " \""++ f ++"\" -> \""++ f' ++"\"\n") (Set.toList sf) + +data S = S { + sErrors :: [(ModulePath, ErrorMessages)], + sWarnings :: [(ModulePath, WarningMessages)], + sGraph :: GmModuleGraph +} + +defaultS :: S +defaultS = S [] [] mempty + +putErr :: MonadState S m + => (ModulePath, ErrorMessages) -> m () +putErr e = do + s <- get + put s { sErrors = e:sErrors s} + +putWarn :: MonadState S m + => (ModulePath, ErrorMessages) -> m () +putWarn w = do + s <- get + put s { sWarnings = w:sWarnings s} + +gmgLookupMP :: MonadState S m => ModulePath -> m (Maybe (Set ModulePath)) +gmgLookupMP k = (Map.lookup k . gmgGraph . sGraph) `liftM` get + +graphUnion :: MonadState S m => GmModuleGraph -> m () +graphUnion gmg = do + s <- get + put s { sGraph = sGraph s `mappend` gmg } + +reachable :: Set ModulePath -> GmModuleGraph -> Set ModulePath +reachable smp0 GmModuleGraph {..} = go smp0 + where + go smp = let + δsmp = Set.unions $ + collapseMaybeSet . flip Map.lookup gmgGraph <$> Set.toList smp + smp' = smp `Set.union` δsmp + in if smp == smp' then smp' else go smp' + +pruneUnreachable :: Set ModulePath -> GmModuleGraph -> GmModuleGraph +pruneUnreachable smp0 gmg@GmModuleGraph {..} = let + r = reachable smp0 gmg + in + GmModuleGraph { + gmgGraph = Map.filterWithKey (\k _ -> k `Set.member` r) gmgGraph + } + +collapseMaybeSet :: Maybe (Set a) -> Set a +collapseMaybeSet = maybe Set.empty id + +homeModuleGraph :: (IOish m, GmLog m, GmEnv m) + => HscEnv -> Set ModulePath -> m GmModuleGraph +homeModuleGraph env smp = updateHomeModuleGraph env mempty smp smp + +mkMainModulePath :: FilePath -> ModulePath +mkMainModulePath = ModulePath (mkModuleName "Main") + +findModulePath :: HscEnv -> ModuleName -> IO (Maybe ModulePath) +findModulePath env mn = do + fmap (ModulePath mn) <$> find env mn + +findModulePathSet :: HscEnv -> [ModuleName] -> IO (Set ModulePath) +findModulePathSet env mns = do + Set.fromList . catMaybes <$> findModulePath env `mapM` mns + +find :: MonadIO m => HscEnv -> ModuleName -> m (Maybe FilePath) +find env mn = liftIO $ do + res <- findHomeModule env mn + case res of + -- TODO: handle SOURCE imports (hs-boot stuff): addBootSuffixLocn loc + Found loc@ModLocation { ml_hs_file = Just _ } _mod -> + return $ normalise <$> ml_hs_file loc + _ -> return Nothing + + +canonicalizeModulePath :: ModulePath -> IO ModulePath +canonicalizeModulePath (ModulePath mn fp) = ModulePath mn <$> canonicalizePath fp + +canonicalizeModuleGraph :: MonadIO m => GmModuleGraph -> m GmModuleGraph +canonicalizeModuleGraph GmModuleGraph {..} = liftIO $ do + GmModuleGraph . Map.fromList <$> mapM fmg (Map.toList gmgGraph) + where + fmg (mp, smp) = liftM2 (,) (canonicalizeModulePath mp) (Set.fromList <$> mapM canonicalizeModulePath (Set.toList smp)) + + +updateHomeModuleGraph :: (IOish m, GmLog m, GmEnv m) + => HscEnv + -> GmModuleGraph + -> Set ModulePath -- ^ Initial set of modules + -> Set ModulePath -- ^ Updated set of modules + -> m GmModuleGraph +updateHomeModuleGraph env GmModuleGraph {..} smp sump = do + -- TODO: It would be good if we could retain information about modules that + -- stop to compile after we've already successfully parsed them at some + -- point. Figure out a way to delete the modules about to be updated only + -- after we're sure they won't fail to parse .. or something. Should probably + -- push this whole prune logic deep into updateHomeModuleGraph' + (pruneUnreachable smp . sGraph) `liftM` runS (updateHomeModuleGraph' env sump) + where + runS = flip execStateT defaultS { sGraph = graph' } + graph' = GmModuleGraph { + gmgGraph = Set.foldr Map.delete gmgGraph sump + } + +mkFileMap :: Set ModulePath -> Map FilePath ModulePath +mkFileMap smp = Map.fromList $ map (mpPath &&& id) $ Set.toList smp + +mkModuleMap :: Set ModulePath -> Map ModuleName ModulePath +mkModuleMap smp = Map.fromList $ map (mpModule &&& id) $ Set.toList smp + +updateHomeModuleGraph' + :: forall m. (MonadState S m, IOish m, GmLog m, GmEnv m) + => HscEnv + -> Set ModulePath -- ^ Initial set of modules + -> m () +updateHomeModuleGraph' env smp0 = do + go `mapM_` Set.toList smp0 + where + go :: ModulePath -> m () + go mp = do + msmp <- gmgLookupMP mp + case msmp of + Just _ -> return () + Nothing -> do + smp <- collapseMaybeSet `liftM` step mp + + graphUnion GmModuleGraph { + gmgGraph = Map.singleton mp smp + } + + mapM_ go (Set.toList smp) + + step :: ModulePath -> m (Maybe (Set ModulePath)) + step mp = runMaybeT $ do + (dflags, ppsrc_fn) <- MaybeT preprocess' + src <- liftIO $ readFile ppsrc_fn + imports mp src dflags + where + preprocess' :: m (Maybe (DynFlags, FilePath)) + preprocess' = do + let fn = mpPath mp + ep <- preprocessFile env fn + case ep of + Right (_, x) -> return $ Just x + Left errs -> do + -- TODO: Remember these and present them as proper errors if this is + -- the file the user is looking at. + gmLog GmWarning ("preprocess " ++ show fn) $ Monoid.mempty $+$ (vcat $ map text errs) + return Nothing + + imports :: ModulePath -> String -> DynFlags -> MaybeT m (Set ModulePath) + imports mp@ModulePath {..} src dflags = + case parseModuleHeader src dflags mpPath of + Left err -> do + putErr (mp, err) + mzero + + Right (ws, lmdl) -> do + putWarn (mp, ws) + let HsModule {..} = unLoc lmdl + mns = map (unLoc . ideclName) + $ filter (isNothing . ideclPkgQual) + $ map unLoc hsmodImports + liftIO $ Set.fromList . catMaybes <$> mapM (findModulePath env) mns + +preprocessFile :: MonadIO m => + HscEnv -> FilePath -> m (Either [String] ([String], (DynFlags, FilePath))) +preprocessFile env file = + liftIO $ withLogger' env $ \setDf -> do + let env' = env { hsc_dflags = setDf (hsc_dflags env) } + preprocess env' (file, Nothing) + +fileModuleName :: + HscEnv -> FilePath -> IO (Either [String] (Maybe ModuleName)) +fileModuleName env fn = handle (\(_ :: SomeException) -> return $ Right Nothing) $ do + ep <- preprocessFile env fn + case ep of + Left errs -> do + return $ Left errs + Right (_warns, (dflags, procdFile)) -> do + src <- readFile procdFile + case parseModuleHeader src dflags procdFile of + Left errs -> do + return $ Left $ errBagToStrList env errs + Right (_, lmdl) -> do + let HsModule {..} = unLoc lmdl + return $ Right $ unLoc <$> hsmodName diff --git a/Language/Haskell/GhcMod/Info.hs b/Language/Haskell/GhcMod/Info.hs index b58b53f..be32635 100644 --- a/Language/Haskell/GhcMod/Info.hs +++ b/Language/Haskell/GhcMod/Info.hs @@ -3,20 +3,25 @@ module Language.Haskell.GhcMod.Info ( , types ) where -import Control.Applicative ((<$>)) +import Control.Applicative import Data.Function (on) import Data.List (sortBy) import Data.Maybe (catMaybes) +import System.FilePath import Exception (ghandle, SomeException(..)) import GHC (GhcMonad, LHsBind, LHsExpr, LPat, Id, TypecheckedModule(..), SrcSpan, Type) +import Prelude import qualified GHC as G -import Language.Haskell.GhcMod.Doc (showPage) -import Language.Haskell.GhcMod.Gap (HasType(..)) import qualified Language.Haskell.GhcMod.Gap as Gap + +import Language.Haskell.GhcMod.Convert +import Language.Haskell.GhcMod.Doc +import Language.Haskell.GhcMod.DynFlags +import Language.Haskell.GhcMod.Gap +import Language.Haskell.GhcMod.Logging import Language.Haskell.GhcMod.Monad import Language.Haskell.GhcMod.SrcUtils import Language.Haskell.GhcMod.Types -import Language.Haskell.GhcMod.Convert ---------------------------------------------------------------- @@ -25,14 +30,22 @@ info :: IOish m => FilePath -- ^ A target file. -> Expression -- ^ A Haskell expression. -> GhcModT m String -info file expr = do - opt <- options - convert opt <$> ghandle handler body +info file expr = + ghandle handler $ + runGmlT' [Left file] deferErrors $ + withContext $ + convert <$> options <*> body where - body = inModuleContext file $ \dflag style -> do - sdoc <- Gap.infoThing expr - return $ showPage dflag style sdoc - handler (SomeException _) = return "Cannot show info" + handler (SomeException ex) = do + gmLog GmException "info" $ text "" $$ nest 4 (showDoc ex) + convert' "Cannot show info" + + body :: GhcMonad m => m String + body = do + sdoc <- Gap.infoThing expr + st <- getStyle + dflag <- G.getSessionDynFlags + return $ showPage dflag st sdoc ---------------------------------------------------------------- @@ -42,24 +55,29 @@ types :: IOish m -> Int -- ^ Line number. -> Int -- ^ Column number. -> GhcModT m String -types file lineNo colNo = do - opt <- options - convert opt <$> ghandle handler body - where - body = inModuleContext file $ \dflag style -> do - modSum <- Gap.fileModSummary file +types file lineNo colNo = + ghandle handler $ + runGmlT' [Left file] deferErrors $ + withContext $ do + crdl <- cradle + modSum <- Gap.fileModSummary (cradleCurrentDir crdl file) srcSpanTypes <- getSrcSpanType modSum lineNo colNo - return $ map (toTup dflag style) $ sortBy (cmp `on` fst) srcSpanTypes - handler (SomeException _) = return [] + dflag <- G.getSessionDynFlags + st <- getStyle + convert' $ map (toTup dflag st) $ sortBy (cmp `on` fst) srcSpanTypes + where + handler (SomeException ex) = do + gmLog GmException "types" $ showDoc ex + return [] getSrcSpanType :: GhcMonad m => G.ModSummary -> Int -> Int -> m [(SrcSpan, Type)] getSrcSpanType modSum lineNo colNo = do - p <- G.parseModule modSum - tcm@TypecheckedModule{tm_typechecked_source = tcs} <- G.typecheckModule p - let bs = listifySpans tcs (lineNo, colNo) :: [LHsBind Id] - es = listifySpans tcs (lineNo, colNo) :: [LHsExpr Id] - ps = listifySpans tcs (lineNo, colNo) :: [LPat Id] - bts <- mapM (getType tcm) bs - ets <- mapM (getType tcm) es - pts <- mapM (getType tcm) ps - return $ catMaybes $ concat [ets, bts, pts] + p <- G.parseModule modSum + tcm@TypecheckedModule{tm_typechecked_source = tcs} <- G.typecheckModule p + let bs = listifySpans tcs (lineNo, colNo) :: [LHsBind Id] + es = listifySpans tcs (lineNo, colNo) :: [LHsExpr Id] + ps = listifySpans tcs (lineNo, colNo) :: [LPat Id] + bts <- mapM (getType tcm) bs + ets <- mapM (getType tcm) es + pts <- mapM (getType tcm) ps + return $ catMaybes $ concat [ets, bts, pts] diff --git a/Language/Haskell/GhcMod/Internal.hs b/Language/Haskell/GhcMod/Internal.hs index d79378c..ea480c8 100644 --- a/Language/Haskell/GhcMod/Internal.hs +++ b/Language/Haskell/GhcMod/Internal.hs @@ -8,35 +8,33 @@ module Language.Haskell.GhcMod.Internal ( , PackageVersion , PackageId , IncludeDir - , CompilerOptions(..) - -- * Cabal API - , parseCabalFile - , getCompilerOptions - , cabalAllBuildInfo - , cabalDependPackages - , cabalSourceDirs - , cabalAllTargets + , GmlT(..) + , MonadIO(..) + , GmEnv(..) -- * Various Paths , ghcLibDir , ghcModExecutable - -- * IO - , getDynamicFlags - -- * Targets - , setTargetFiles -- * Logging , withLogger , setNoWarningFlags , setAllWarningFlags -- * Environment, state and logging , GhcModEnv(..) - , newGhcModEnv , GhcModState - , defaultState , CompilerMode(..) , GhcModLog + , GmLog(..) + , GmLogLevel(..) + , gmSetLogLevel -- * Monad utilities , runGhcModT' , hoistGhcModT + , runGmlT + , runGmlT' + , gmlGetSession + , gmlSetSession + , loadTargets + , cabalResolvedComponents -- ** Accessing 'GhcModEnv' and 'GhcModState' , options , cradle @@ -45,28 +43,33 @@ module Language.Haskell.GhcMod.Internal ( , withOptions -- * 'GhcModError' , gmeDoc - -- * 'GhcMonad' Choice - , (||>) - , goNext - , runAnyOne -- * World , World , getCurrentWorld , didWorldChange + -- * Cabal Helper + , ModulePath(..) + , GmComponent(..) + , GmComponentType(..) + , GmModuleGraph(..) + , prepareCabalHelper + -- * Misc stuff + , GHandler(..) + , gcatches ) where import GHC.Paths (libdir) -import Language.Haskell.GhcMod.CabalApi +import Language.Haskell.GhcMod.Target import Language.Haskell.GhcMod.DynFlags import Language.Haskell.GhcMod.Error -import Language.Haskell.GhcMod.GHCChoice import Language.Haskell.GhcMod.Logger +import Language.Haskell.GhcMod.Logging import Language.Haskell.GhcMod.Monad -import Language.Haskell.GhcMod.Target import Language.Haskell.GhcMod.Types import Language.Haskell.GhcMod.Utils import Language.Haskell.GhcMod.World +import Language.Haskell.GhcMod.CabalHelper -- | Obtaining the directory for ghc system libraries. ghcLibDir :: FilePath diff --git a/Language/Haskell/GhcMod/Logger.hs b/Language/Haskell/GhcMod/Logger.hs index 00f9625..3fbd436 100644 --- a/Language/Haskell/GhcMod/Logger.hs +++ b/Language/Haskell/GhcMod/Logger.hs @@ -1,31 +1,33 @@ -{-# LANGUAGE CPP #-} - module Language.Haskell.GhcMod.Logger ( withLogger - , withLoggerTwice + , withLogger' , checkErrorPrefix + , errsToStr + , errBagToStrList ) where -import Bag (Bag, bagToList, emptyBag, consBag, filterBag, unionBags) -import Control.Applicative ((<$>)) +import Control.Arrow +import Control.Applicative +import Data.List (isPrefixOf) +import Data.Maybe (fromMaybe) import Data.IORef (IORef, newIORef, readIORef, writeIORef, modifyIORef) -import Data.List (isPrefixOf, find, nub, isInfixOf) -import Data.Maybe (fromMaybe, isJust) -import ErrUtils (ErrMsg, WarnMsg, errMsgShortDoc, errMsgExtraInfo, mkWarnMsg) -import Exception (ghandle) -import GHC (DynFlags, SrcSpan, Severity(SevError)) -import qualified GHC as G -import HscTypes (SourceError, srcErrorMessages) -import Language.Haskell.GhcMod.Doc (showPage, getStyle) -import Language.Haskell.GhcMod.DynFlags (withDynFlags, withCmdFlags) -import qualified Language.Haskell.GhcMod.Gap as Gap -import Language.Haskell.GhcMod.Convert (convert') -import Language.Haskell.GhcMod.Monad -import Language.Haskell.GhcMod.Types -import Outputable (PprStyle, SDoc, qualName, qualModule, mkErrStyle, neverQualify) import System.FilePath (normalise) +import Text.PrettyPrint ----------------------------------------------------------------- +import ErrUtils (ErrMsg, errMsgShortDoc, errMsgExtraInfo) +import GHC (DynFlags, SrcSpan, Severity(SevError)) +import HscTypes +import Outputable +import qualified GHC as G +import Bag + +import Language.Haskell.GhcMod.Convert +import Language.Haskell.GhcMod.Doc (showPage) +import Language.Haskell.GhcMod.DynFlags (withDynFlags) +import Language.Haskell.GhcMod.Monad.Types +import Language.Haskell.GhcMod.Error +import qualified Language.Haskell.GhcMod.Gap as Gap +import Prelude type Builder = [String] -> [String] @@ -39,178 +41,94 @@ emptyLog = Log [] id newLogRef :: IO LogRef newLogRef = LogRef <$> newIORef emptyLog -readAndClearLogRef :: IOish m => LogRef -> GhcModT m String +readAndClearLogRef :: LogRef -> IO [String] readAndClearLogRef (LogRef ref) = do - Log _ b <- liftIO $ readIORef ref - liftIO $ writeIORef ref emptyLog - convert' (b []) + Log _ b <- readIORef ref + writeIORef ref emptyLog + return $ b [] appendLogRef :: DynFlags -> LogRef -> DynFlags -> Severity -> SrcSpan -> PprStyle -> SDoc -> IO () -appendLogRef df (LogRef ref) _ sev src style msg = modifyIORef ref update +appendLogRef df (LogRef ref) _ sev src st msg = modifyIORef ref update where - l = ppMsg src sev df style msg + l = ppMsg src sev df st msg update lg@(Log ls b) | l `elem` ls = lg | otherwise = Log (l:ls) (b . (l:)) ---------------------------------------------------------------- -data LogBag = LogBag (Bag WarnMsg) -newtype LogBagRef = LogBagRef (IORef LogBag) - -emptyLogBag :: LogBag -emptyLogBag = LogBag emptyBag - -newLogBagRef :: IO LogBagRef -newLogBagRef = LogBagRef <$> newIORef emptyLogBag - -readAndClearLogBagRef :: IOish m => LogBagRef -> GhcModT m (Bag WarnMsg) -readAndClearLogBagRef (LogBagRef ref) = do - LogBag b <- liftIO $ readIORef ref - liftIO $ writeIORef ref emptyLogBag - return b - -appendLogBagRef :: DynFlags -> LogBagRef -> DynFlags -> Severity -> SrcSpan -> PprStyle -> SDoc -> IO () -appendLogBagRef df (LogBagRef ref) _ _ src style msg = modifyIORef ref update - where - qstyle = (qualName style, qualModule style) -#if __GLASGOW_HASKELL__ >= 706 - warnMsg = mkWarnMsg df src qstyle msg -#else - warnMsg = mkWarnMsg src qstyle msg -#endif - warnBag = consBag warnMsg emptyBag - update (LogBag b) = let (b1,b2) = mergeErrors df style b warnBag - in LogBag $ b1 `unionBags` b2 - ----------------------------------------------------------------- - -- | Set the session flag (e.g. "-Wall" or "-w:") then -- executes a body. Logged messages are returned as 'String'. -- Right is success and Left is failure. -withLogger :: IOish m +withLogger :: (GmGhc m, GmEnv m) => (DynFlags -> DynFlags) - -> GhcModT m () - -> GhcModT m (Either String String) -withLogger setDF body = ghandle sourceError $ do - logref <- liftIO newLogRef - wflags <- filter ("-fno-warn" `isPrefixOf`) . ghcUserOptions <$> options - withDynFlags (setLogger logref . setDF) $ - withCmdFlags wflags $ do - body - Right <$> readAndClearLogRef logref + -> m a + -> m (Either String (String, a)) +withLogger f action = do + env <- G.getSession + opts <- options + let conv = convert opts + eres <- withLogger' env $ \setDf -> + withDynFlags (f . setDf) action + return $ either (Left . conv) (Right . first conv) eres + +withLogger' :: IOish m + => HscEnv -> ((DynFlags -> DynFlags) -> m a) -> m (Either [String] ([String], a)) +withLogger' env action = do + logref <- liftIO $ newLogRef + + let dflags = hsc_dflags env + pu = icPrintUnqual dflags (hsc_IC env) + st = mkUserStyle pu AllTheWay + + fn df = setLogger logref df + + a <- gcatches (Right <$> action fn) (handlers dflags st) + ls <- liftIO $ readAndClearLogRef logref + + return $ ((,) ls <$> a) + where setLogger logref df = Gap.setLogAction df $ appendLogRef df logref + handlers df st = [ + GHandler $ \ex -> return $ Left $ sourceError df st ex, + GHandler $ \ex -> return $ Left [render $ ghcExceptionDoc ex] + ] -withLoggerTwice :: IOish m - => (DynFlags -> DynFlags) - -> GhcModT m () - -> (DynFlags -> DynFlags) - -> GhcModT m () - -> GhcModT m (Either String String) -withLoggerTwice setDF1 body1 setDF2 body2 = do - err1 <- ghandle sourceErrorBag $ do - logref <- liftIO newLogBagRef - wflags <- filter ("-fno-warn" `isPrefixOf`) . ghcUserOptions <$> options - withDynFlags (setLogger logref . setDF1) $ - withCmdFlags wflags $ do - body1 - Right <$> readAndClearLogBagRef logref - err2 <- ghandle sourceErrorBag $ do - logref <- liftIO newLogBagRef - wflags <- filter ("-fno-warn" `isPrefixOf`) . ghcUserOptions <$> options - withDynFlags (setLogger logref . setDF2) $ - withCmdFlags wflags $ do - body2 - Right <$> readAndClearLogBagRef logref - -- Merge errors and warnings - dflags <- G.getSessionDynFlags - style <- getStyle - case (err1, err2) of - (Right b1, Right b2) -> do let (warn1,_) = mergeErrors dflags style b1 b2 - errAndWarnBagToStr Right emptyBag (warn1 `unionBags` b2) - (Left b1, Right b2) -> do let (err,warn) = mergeErrors dflags style b1 b2 - errAndWarnBagToStr Right err warn - (Right b1, Left b2) -> do let (err,warn) = mergeErrors dflags style b2 b1 - errAndWarnBagToStr Right err warn - (Left b1, Left b2) -> do let (err1',err2') = mergeErrors dflags style b1 b2 - errAndWarnBagToStr Right (err1' `unionBags` err2') emptyBag - where - setLogger logref df = Gap.setLogAction df $ appendLogBagRef df logref +errBagToStrList :: HscEnv -> Bag ErrMsg -> [String] +errBagToStrList env errs = let + dflags = hsc_dflags env + pu = icPrintUnqual dflags (hsc_IC env) + st = mkUserStyle pu AllTheWay + in errsToStr dflags st $ bagToList errs ---------------------------------------------------------------- -- | Converting 'SourceError' to 'String'. -sourceError :: IOish m => SourceError -> GhcModT m (Either String String) -sourceError err = errBagToStr (srcErrorMessages err) +sourceError :: DynFlags -> PprStyle -> SourceError -> [String] +sourceError df st src_err = errsToStr df st $ reverse $ bagToList $ srcErrorMessages src_err -errBagToStr :: IOish m => Bag ErrMsg -> GhcModT m (Either String String) -errBagToStr = errBagToStr' Left - -errBagToStr' :: IOish m => (String -> a) -> Bag ErrMsg -> GhcModT m a -errBagToStr' f err = do - dflags <- G.getSessionDynFlags - style <- getStyle - ret <- convert' (errBagToStrList dflags style err) - return $ f ret - -errAndWarnBagToStr :: IOish m => (String -> a) -> Bag ErrMsg -> Bag WarnMsg -> GhcModT m a -errAndWarnBagToStr f err warn = do - dflags <- G.getSessionDynFlags - -- style <- toGhcModT getStyle -#if __GLASGOW_HASKELL__ >= 706 - let style = mkErrStyle dflags neverQualify -#else - let style = mkErrStyle neverQualify -#endif - ret <- convert' $ nub (errBagToStrList dflags style err ++ warnBagToStrList dflags style warn) - return $ f ret - -errBagToStrList :: DynFlags -> PprStyle -> Bag ErrMsg -> [String] -errBagToStrList dflag style = map (ppErrMsg dflag style) . reverse . bagToList - -warnBagToStrList :: DynFlags -> PprStyle -> Bag WarnMsg -> [String] -warnBagToStrList dflag style = map (ppWarnMsg dflag style) . reverse . bagToList - -sourceErrorBag :: IOish m => SourceError -> GhcModT m (Either (Bag ErrMsg) (Bag WarnMsg)) -sourceErrorBag err = return $ Left (srcErrorMessages err) - -mergeErrors :: DynFlags -> PprStyle -> Bag ErrMsg -> Bag ErrMsg -> (Bag ErrMsg, Bag ErrMsg) -mergeErrors dflag style b1 b2 = - let b1Msgs = map (\err1 -> let m = ppWarnMsg dflag style err1 in (m, head $ lines m)) - (bagToList b1) - mustBeB2 = \err2 -> let msg2 = ppWarnMsg dflag style err2 - line2 = head $ lines msg2 - in not . isJust $ find (\(msg1, line1) -> msg1 == msg2 || (line1 == line2 && isHoleMsg line1)) b1Msgs - in (b1, filterBag mustBeB2 b2) - -isHoleMsg :: String -> Bool -isHoleMsg = isInfixOf "Found hole" +errsToStr :: DynFlags -> PprStyle -> [ErrMsg] -> [String] +errsToStr df st = map (ppErrMsg df st) ---------------------------------------------------------------- ppErrMsg :: DynFlags -> PprStyle -> ErrMsg -> String -ppErrMsg dflag style err = ppMsg spn SevError dflag style msg ++ (if null ext then "" else "\n" ++ ext) +ppErrMsg dflag st err = + ppMsg spn SevError dflag st msg ++ (if null ext then "" else "\n" ++ ext) where spn = Gap.errorMsgSpan err msg = errMsgShortDoc err - ext = showPage dflag style (errMsgExtraInfo err) - -ppWarnMsg :: DynFlags -> PprStyle -> ErrMsg -> String -ppWarnMsg dflag style err = ppMsg spn G.SevWarning dflag style msg ++ (if null ext then "" else "\n" ++ ext) - where - spn = Gap.errorMsgSpan err - msg = errMsgShortDoc err - ext = showPage dflag style (errMsgExtraInfo err) + ext = showPage dflag st (errMsgExtraInfo err) ppMsg :: SrcSpan -> Severity-> DynFlags -> PprStyle -> SDoc -> String -ppMsg spn sev dflag style msg = prefix ++ cts +ppMsg spn sev dflag st msg = prefix ++ cts where - cts = showPage dflag style msg - prefix = ppMsgPrefix spn sev dflag style cts + cts = showPage dflag st msg + prefix = ppMsgPrefix spn sev dflag st cts ppMsgPrefix :: SrcSpan -> Severity-> DynFlags -> PprStyle -> String -> String -ppMsgPrefix spn sev dflag _style cts = +ppMsgPrefix spn sev dflag _st cts = let defaultPrefix | Gap.isDumpSplices dflag = "" | otherwise = checkErrorPrefix diff --git a/Language/Haskell/GhcMod/Logging.hs b/Language/Haskell/GhcMod/Logging.hs new file mode 100644 index 0000000..a7a1bea --- /dev/null +++ b/Language/Haskell/GhcMod/Logging.hs @@ -0,0 +1,102 @@ +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +module Language.Haskell.GhcMod.Logging ( + module Language.Haskell.GhcMod.Logging + , module Language.Haskell.GhcMod.Pretty + , GmLogLevel(..) + , module Text.PrettyPrint + , module Data.Monoid + ) where + +import Control.Applicative hiding (empty) +import Control.Monad +import Control.Monad.Trans.Class +import Data.List +import Data.Char +import Data.Monoid +import Data.Maybe +import System.IO +import System.FilePath +import Text.PrettyPrint hiding (style, (<>)) +import Prelude + +import Language.Haskell.GhcMod.Monad.Types +import Language.Haskell.GhcMod.Types +import Language.Haskell.GhcMod.Pretty +import Language.Haskell.GhcMod.Output + +gmSetLogLevel :: GmLog m => GmLogLevel -> m () +gmSetLogLevel level = + gmlJournal $ GhcModLog (Just level) (Last Nothing) [] + +gmSetDumpLevel :: GmLog m => Bool -> m () +gmSetDumpLevel level = + gmlJournal $ GhcModLog Nothing (Last (Just level)) [] + + +increaseLogLevel :: GmLogLevel -> GmLogLevel +increaseLogLevel l | l == maxBound = l +increaseLogLevel l = succ l + +decreaseLogLevel :: GmLogLevel -> GmLogLevel +decreaseLogLevel l | l == minBound = l +decreaseLogLevel l = pred l + +-- | +-- >>> Just GmDebug <= Nothing +-- False +-- >>> Just GmException <= Just GmDebug +-- True +-- >>> Just GmDebug <= Just GmException +-- False +gmLog :: (MonadIO m, GmLog m, GmEnv m) => GmLogLevel -> String -> Doc -> m () +gmLog level loc' doc = do + GhcModLog { gmLogLevel = Just level' } <- gmlHistory + + let loc | loc' == "" = empty + | otherwise = text loc' <+>: empty + msgDoc = gmLogLevelDoc level <+>: sep [loc, doc] + msg = dropWhileEnd isSpace $ gmRenderDoc msgDoc + + when (level <= level') $ gmErrStrLn msg + + gmlJournal (GhcModLog Nothing (Last Nothing) [(level, loc', msgDoc)]) + +gmVomit :: (MonadIO m, GmLog m, GmEnv m) => String -> Doc -> String -> m () +gmVomit filename doc content = do + gmLog GmVomit "" $ doc <+>: text content + + GhcModLog { gmLogVomitDump = Last mdump } + <- gmlHistory + + dir <- cradleTempDir `liftM` cradle + when (fromMaybe False mdump) $ + liftIO $ writeFile (dir filename) content + + +newtype LogDiscardT m a = LogDiscardT { runLogDiscard :: m a } + deriving (Functor, Applicative, Monad) + +instance MonadTrans LogDiscardT where + lift = LogDiscardT + +instance Monad m => GmLog (LogDiscardT m) where + gmlJournal = const $ return () + gmlHistory = return mempty + gmlClear = return () diff --git a/Language/Haskell/GhcMod/Modules.hs b/Language/Haskell/GhcMod/Modules.hs index cea00d7..a5766c6 100644 --- a/Language/Haskell/GhcMod/Modules.hs +++ b/Language/Haskell/GhcMod/Modules.hs @@ -1,32 +1,26 @@ module Language.Haskell.GhcMod.Modules (modules) where -import Control.Applicative ((<$>)) -import Control.Exception (SomeException(..)) -import Data.List (nub, sort) -import qualified GHC as G +import Control.Arrow +import Data.List import Language.Haskell.GhcMod.Convert -import Language.Haskell.GhcMod.Monad import Language.Haskell.GhcMod.Types -import Packages (pkgIdMap, exposedModules, sourcePackageId, display) -import UniqFM (eltsUFM) +import Language.Haskell.GhcMod.Monad +import Language.Haskell.GhcMod.Gap ( listVisibleModuleNames + , lookupModulePackageInAllPackages + ) + +import qualified GHC as G ---------------------------------------------------------------- -- | Listing installed modules. -modules :: IOish m => GhcModT m String +modules :: (IOish m, GmEnv m, GmState m, GmLog m) => m String modules = do - opt <- options - convert opt . arrange opt <$> (getModules `G.gcatch` handler) - where - getModules = getExposedModules <$> G.getSessionDynFlags - getExposedModules = concatMap exposedModules' - . eltsUFM . pkgIdMap . G.pkgState - exposedModules' p = - map G.moduleNameString (exposedModules p) - `zip` - repeat (display $ sourcePackageId p) - arrange opt = nub . sort . map (dropPkgs opt) - dropPkgs opt (name, pkg) - | detailed opt = name ++ " " ++ pkg - | otherwise = name - handler (SomeException _) = return [] + Options { detailed } <- options + df <- runGmPkgGhc G.getSessionDynFlags + let mns = listVisibleModuleNames df + pmnss = map (first moduleNameString) $ zip mns (modulePkg df `map` mns) + convert' $ nub [ if detailed then pkg ++ " " ++ mn else mn + | (mn, pkgs) <- pmnss, pkg <- pkgs ] + where + modulePkg df = lookupModulePackageInAllPackages df diff --git a/Language/Haskell/GhcMod/Monad.hs b/Language/Haskell/GhcMod/Monad.hs index 3b3b697..adc7114 100644 --- a/Language/Haskell/GhcMod/Monad.hs +++ b/Language/Haskell/GhcMod/Monad.hs @@ -1,289 +1,100 @@ -{-# LANGUAGE CPP, GeneralizedNewtypeDeriving, FlexibleInstances #-} -{-# LANGUAGE FlexibleContexts, MultiParamTypeClasses, RankNTypes #-} -{-# LANGUAGE TypeFamilies, UndecidableInstances, RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . +{-# LANGUAGE CPP #-} module Language.Haskell.GhcMod.Monad ( - -- * Monad Types - GhcModT - , IOish - -- ** Environment, state and logging - , GhcModEnv(..) - , newGhcModEnv - , GhcModState(..) - , defaultState - , CompilerMode(..) - , GhcModLog - , GhcModError(..) - -- * Monad utilities - , runGhcModT + runGhcModT , runGhcModT' + , runGhcModT'' , hoistGhcModT - -- ** Accessing 'GhcModEnv' and 'GhcModState' - , gmsGet - , gmsPut - , options - , cradle - , getCompilerMode - , setCompilerMode - , withOptions - , withTempSession - , overrideGhcUserOptions - -- ** Re-exporting convenient stuff - , liftIO - , module Control.Monad.Reader.Class - , module Control.Monad.Journal.Class + , runGmlT + , runGmlT' + , runGmlTWith + , runGmPkgGhc + , withGhcModEnv + , withGhcModEnv' + , module Language.Haskell.GhcMod.Monad.Types ) where -#if __GLASGOW_HASKELL__ < 708 --- 'CoreMonad.MonadIO' and 'Control.Monad.IO.Class.MonadIO' are different --- classes before ghc 7.8 -#define DIFFERENT_MONADIO 1 - --- RWST doen't have a MonadIO instance before ghc 7.8 -#define MONADIO_INSTANCES 1 -#endif - - import Language.Haskell.GhcMod.Types +import Language.Haskell.GhcMod.Monad.Types import Language.Haskell.GhcMod.Error +import Language.Haskell.GhcMod.Logging import Language.Haskell.GhcMod.Cradle -import Language.Haskell.GhcMod.DynFlags -import Language.Haskell.GhcMod.GhcPkg -import Language.Haskell.GhcMod.CabalApi -import qualified Language.Haskell.GhcMod.Gap as Gap +import Language.Haskell.GhcMod.Target +import Language.Haskell.GhcMod.Output -import DynFlags -import GHC -import qualified GHC as G -import GHC.Paths (libdir) -import GhcMonad hiding (withTempSession) -#if __GLASGOW_HASKELL__ <= 702 -import HscTypes -#endif - --- MonadUtils of GHC 7.6 or earlier defines its own MonadIO. --- RWST does not automatically become an instance of MonadIO. --- MonadUtils of GHC 7.8 or later imports MonadIO in Monad.Control.IO.Class. --- So, RWST automatically becomes an instance of MonadIO. -import MonadUtils - -#if DIFFERENT_MONADIO -import Control.Monad.Trans.Class (lift) -import qualified Control.Monad.IO.Class -import Data.Monoid (Monoid) -#endif - -import Control.Applicative (Alternative) import Control.Arrow (first) -import Control.Monad (MonadPlus, void) -#if !MIN_VERSION_monad_control(1,0,0) -import Control.Monad (liftM) -#endif -import Control.Monad.Base (MonadBase, liftBase) +import Control.Applicative --- Monad transformer stuff -import Control.Monad.Trans.Control (MonadBaseControl(..), StM, liftBaseWith, - control, liftBaseOp, liftBaseOp_) +import Control.Concurrent -import Control.Monad.Trans.Class -import Control.Monad.Reader.Class -import Control.Monad.Writer.Class (MonadWriter) -import Control.Monad.State.Class (MonadState(..)) +import Control.Monad.Reader (runReaderT) +import Control.Monad.State.Strict (runStateT) +import Control.Monad.Trans.Journal (runJournalT) -import Control.Monad.Error (ErrorT, runErrorT) -import Control.Monad.Reader (ReaderT, runReaderT) -import Control.Monad.State.Strict (StateT, runStateT) -import Control.Monad.Trans.Journal (JournalT, runJournalT) -#ifdef MONADIO_INSTANCES -import Control.Monad.Trans.Maybe (MaybeT) -import Control.Monad.Error (Error(..)) -#endif -import Control.Monad.Journal.Class +import Exception (ExceptionMonad(..)) -import Data.Maybe (isJust) -import Data.IORef (IORef, readIORef, writeIORef, newIORef) -import System.Directory (getCurrentDirectory) +import System.Directory +import Prelude ----------------------------------------------------------------- +withCradle :: IOish m => FilePath -> (Cradle -> m a) -> m a +withCradle cradledir f = + gbracket (liftIO $ findCradle' cradledir) (liftIO . cleanupCradle) f -data GhcModEnv = GhcModEnv { - gmGhcSession :: !(IORef HscEnv) - , gmOptions :: Options - , gmCradle :: Cradle - } +withGhcModEnv :: IOish m => FilePath -> Options -> (GhcModEnv -> m a) -> m a +withGhcModEnv dir opt f = withCradle dir (withGhcModEnv' opt f) -type GhcModLog = () +withGhcModEnv' :: IOish m => Options -> (GhcModEnv -> m a) -> Cradle -> m a +withGhcModEnv' opt f crdl = do + olddir <- liftIO getCurrentDirectory + c <- liftIO newChan + let outp = case linePrefix opt of + Just _ -> GmOutputChan c + Nothing -> GmOutputStdio + gbracket_ (setup c) (teardown olddir) (f $ GhcModEnv opt crdl outp) + where + setup c = liftIO $ do + setCurrentDirectory $ cradleRootDir crdl + forkIO $ stdoutGateway c -data GhcModState = GhcModState { - gmCompilerMode :: CompilerMode - } deriving (Eq,Show,Read) + teardown olddir tid = liftIO $ do + setCurrentDirectory olddir + killThread tid -data CompilerMode = Simple | Intelligent deriving (Eq,Show,Read) - -defaultState :: GhcModState -defaultState = GhcModState Simple - ----------------------------------------------------------------- - --- | This is basically a newtype wrapper around 'StateT', 'ErrorT', 'JournalT' --- and 'ReaderT' with custom instances for 'GhcMonad' and it's constraints that --- means you can run (almost) all functions from the GHC API on top of 'GhcModT' --- transparently. --- --- The inner monad @m@ should have instances for 'MonadIO' and --- 'MonadBaseControl' 'IO', in the common case this is simply 'IO'. Most @mtl@ --- monads already have 'MonadBaseControl' 'IO' instances, see the --- @monad-control@ package. -newtype GhcModT m a = GhcModT { - unGhcModT :: StateT GhcModState - (ErrorT GhcModError - (JournalT GhcModLog - (ReaderT GhcModEnv m) ) ) a - } deriving ( Functor - , Applicative - , Alternative - , Monad - , MonadPlus -#if DIFFERENT_MONADIO - , Control.Monad.IO.Class.MonadIO -#endif - , MonadReader GhcModEnv -- TODO: make MonadReader instance - -- pass-through like MonadState - , MonadWriter w - , MonadError GhcModError - ) - -instance MonadIO m => MonadIO (GhcModT m) where - liftIO action = do - res <- GhcModT . liftIO . liftIO . liftIO . liftIO $ try action - case res of - Right a -> return a - - Left e | isIOError e -> - throwError $ GMEIOException (fromEx e :: IOError) - Left e | isGhcModError e -> - throwError $ (fromEx e :: GhcModError) - Left e -> throw e - - where - fromEx :: Exception e => SomeException -> e - fromEx se = let Just e = fromException se in e - isIOError se = - case fromException se of - Just (_ :: IOError) -> True - Nothing -> False - - isGhcModError se = - case fromException se of - Just (_ :: GhcModError) -> True - Nothing -> False - - -instance MonadTrans (GhcModT) where - lift = GhcModT . lift . lift . lift . lift - -instance MonadState s m => MonadState s (GhcModT m) where - get = GhcModT $ lift $ lift $ lift get - put = GhcModT . lift . lift . lift . put - state = GhcModT . lift . lift . lift . state - - -#if MONADIO_INSTANCES -instance MonadIO m => MonadIO (StateT s m) where - liftIO = lift . liftIO - -instance MonadIO m => MonadIO (ReaderT r m) where - liftIO = lift . liftIO - -instance (Monoid w, MonadIO m) => MonadIO (JournalT w m) where - liftIO = lift . liftIO - -instance (Error e, MonadIO m) => MonadIO (ErrorT e m) where - liftIO = lift . liftIO - -instance MonadIO m => MonadIO (MaybeT m) where - liftIO = lift . liftIO -#endif - ----------------------------------------------------------------- - --- | Initialize the 'DynFlags' relating to the compilation of a single --- file or GHC session according to the 'Cradle' and 'Options' --- provided. -initializeFlagsWithCradle :: (IOish m, GhcMonad m, MonadError GhcModError m) - => Options - -> Cradle - -> m () -initializeFlagsWithCradle opt c - | cabal = withCabal - | otherwise = withSandbox - where - mCabalFile = cradleCabalFile c - cabal = isJust mCabalFile - ghcopts = ghcUserOptions opt - withCabal = do - let Just cabalFile = mCabalFile - pkgDesc <- parseCabalFile c cabalFile - compOpts <- getCompilerOptions ghcopts c pkgDesc - initSession CabalPkg opt compOpts - withSandbox = initSession SingleFile opt compOpts - where - importDirs = [".","..","../..","../../..","../../../..","../../../../.."] - pkgOpts = ghcDbStackOpts $ cradlePkgDbStack c - compOpts - | null pkgOpts = CompilerOptions ghcopts importDirs [] - | otherwise = CompilerOptions (ghcopts ++ pkgOpts) [wdir,rdir] [] - wdir = cradleCurrentDir c - rdir = cradleRootDir c - -initSession :: GhcMonad m - => Build - -> Options - -> CompilerOptions - -> m () -initSession build Options {..} CompilerOptions {..} = do - df <- G.getSessionDynFlags - void $ G.setSessionDynFlags =<< addCmdOpts ghcOptions - ( setModeSimple - $ Gap.setFlags - $ setIncludeDirs includeDirs - $ setBuildEnv build - $ setEmptyLogger - $ Gap.addPackageFlags depPackages df) - ----------------------------------------------------------------- - -newGhcModEnv :: Options -> FilePath -> IO GhcModEnv -newGhcModEnv opt dir = do - session <- newIORef (error "empty session") - c <- findCradle' dir - return GhcModEnv { - gmGhcSession = session - , gmOptions = opt - , gmCradle = c - } - -cleanupGhcModEnv :: GhcModEnv -> IO () -cleanupGhcModEnv env = cleanupCradle $ gmCradle env + gbracket_ ma mb mc = gbracket ma mb (const mc) -- | Run a @GhcModT m@ computation. runGhcModT :: IOish m => Options -> GhcModT m a -> m (Either GhcModError a, GhcModLog) -runGhcModT opt action = gbracket newEnv delEnv $ \env -> do - r <- first (fst <$>) <$> (runGhcModT' env defaultState $ do - dflags <- getSessionDynFlags - defaultCleanupHandler dflags $ do - initializeFlagsWithCradle opt (gmCradle env) - action) - return r +runGhcModT opt action = do + dir <- liftIO getCurrentDirectory + runGhcModT' dir opt action - where - newEnv = liftBase $ newGhcModEnv opt =<< getCurrentDirectory - delEnv = liftBase . cleanupGhcModEnv +runGhcModT' :: IOish m + => FilePath + -> Options + -> GhcModT m a + -> m (Either GhcModError a, GhcModLog) +runGhcModT' dir opt action = liftIO (canonicalizePath dir) >>= \dir' -> + withGhcModEnv dir' opt $ \env -> + first (fst <$>) <$> runGhcModT'' env defaultGhcModState + (gmSetLogLevel (logLevel opt) >> action) -- | @hoistGhcModT result@. Embed a GhcModT computation's result into a GhcModT -- computation. Note that if the computation that returned @result@ modified the @@ -292,7 +103,7 @@ hoistGhcModT :: IOish m => (Either GhcModError a, GhcModLog) -> GhcModT m a hoistGhcModT (r,l) = do - GhcModT (lift $ lift $ journal l) >> case r of + gmlJournal l >> case r of Left e -> throwError e Right a -> return a @@ -301,179 +112,10 @@ hoistGhcModT (r,l) = do -- do with 'GhcModEnv' and 'GhcModState'. -- -- You should probably look at 'runGhcModT' instead. -runGhcModT' :: IOish m - => GhcModEnv - -> GhcModState - -> GhcModT m a - -> m (Either GhcModError (a, GhcModState), GhcModLog) -runGhcModT' r s a = do - (res, w') <- - flip runReaderT r $ runJournalT $ runErrorT $ - runStateT (unGhcModT $ initGhcMonad (Just libdir) >> a) s - return (res, w') ----------------------------------------------------------------- --- | Make a copy of the 'gmGhcSession' IORef, run the action and restore the --- original 'HscEnv'. -withTempSession :: IOish m => GhcModT m a -> GhcModT m a -withTempSession action = do - session <- gmGhcSession <$> ask - savedHscEnv <- liftIO $ readIORef session - a <- action - liftIO $ writeIORef session savedHscEnv - return a - --- | This is a very ugly workaround don't use it. -overrideGhcUserOptions :: IOish m => ([GHCOption] -> GhcModT m b) -> GhcModT m b -overrideGhcUserOptions action = withTempSession $ do - env <- ask - opt <- options - let ghcOpts = ghcUserOptions opt - opt' = opt { ghcUserOptions = [] } - - initializeFlagsWithCradle opt' (gmCradle env) - - action ghcOpts - ----------------------------------------------------------------- - -gmeAsk :: IOish m => GhcModT m GhcModEnv -gmeAsk = ask - -gmsGet :: IOish m => GhcModT m GhcModState -gmsGet = GhcModT get - -gmsPut :: IOish m => GhcModState -> GhcModT m () -gmsPut = GhcModT . put - -options :: IOish m => GhcModT m Options -options = gmOptions <$> gmeAsk - -cradle :: IOish m => GhcModT m Cradle -cradle = gmCradle <$> gmeAsk - -getCompilerMode :: IOish m => GhcModT m CompilerMode -getCompilerMode = gmCompilerMode <$> gmsGet - -setCompilerMode :: IOish m => CompilerMode -> GhcModT m () -setCompilerMode mode = (\s -> gmsPut s { gmCompilerMode = mode } ) =<< gmsGet - ----------------------------------------------------------------- - -withOptions :: IOish m => (Options -> Options) -> GhcModT m a -> GhcModT m a -withOptions changeOpt action = local changeEnv action - where - changeEnv e = e { gmOptions = changeOpt opt } - where - opt = gmOptions e - ----------------------------------------------------------------- - -instance (MonadBaseControl IO m) => MonadBase IO (GhcModT m) where - liftBase = GhcModT . liftBase - -#if MIN_VERSION_monad_control(1,0,0) - -instance (MonadBaseControl IO m) => MonadBaseControl IO (GhcModT m) where - type StM (GhcModT m) a = - StM (StateT GhcModState - (ErrorT GhcModError - (JournalT GhcModLog - (ReaderT GhcModEnv m) ) ) ) a - liftBaseWith f = GhcModT . liftBaseWith $ \runInBase -> - f $ runInBase . unGhcModT - - restoreM = GhcModT . restoreM - {-# INLINE liftBaseWith #-} - {-# INLINE restoreM #-} - -#else - -instance (MonadBaseControl IO m) => MonadBaseControl IO (GhcModT m) where - newtype StM (GhcModT m) a = StGhcMod { - unStGhcMod :: StM (StateT GhcModState - (ErrorT GhcModError - (JournalT GhcModLog - (ReaderT GhcModEnv m) ) ) ) a } - liftBaseWith f = GhcModT . liftBaseWith $ \runInBase -> - f $ liftM StGhcMod . runInBase . unGhcModT - - restoreM = GhcModT . restoreM . unStGhcMod - {-# INLINE liftBaseWith #-} - {-# INLINE restoreM #-} - -#endif - --- GHC cannot prove the following instances to be decidable automatically using --- the FlexibleContexts extension as they violate the second Paterson Condition, --- namely that: The assertion has fewer constructors and variables (taken --- together and counting repetitions) than the head. Specifically the --- @MonadBaseControl IO m@ constraint is causing this violation. --- --- Proof of termination: --- --- Assuming all constraints containing the variable `m' exist and are decidable --- we show termination by manually replacing the current set of constraints with --- their own set of constraints and show that this, after a finite number of --- steps, results in the empty set, i.e. not having to check any more --- constraints. --- --- We start by setting the constraints to be those immediate constraints of the --- instance declaration which cannot be proven decidable automatically for the --- type under consideration. --- --- @ --- { MonadBaseControl IO m } --- @ --- --- Classes used: --- --- * @class MonadBase b m => MonadBaseControl b m@ --- --- @ --- { MonadBase IO m } --- @ --- --- Classes used: --- --- * @class (Applicative b, Applicative m, Monad b, Monad m) => MonadBase b m@ --- --- @ --- { Applicative IO, Applicative m, Monad IO, Monad m } --- @ --- --- Classes used: --- --- * @class Monad m@ --- * @class Applicative f => Functor f@ --- --- @ --- { Functor m } --- @ --- --- Classes used: --- --- * @class Functor f@ --- --- @ --- { } --- @ --- ∎ - -instance (Functor m, MonadIO m, MonadBaseControl IO m) - => GhcMonad (GhcModT m) where - getSession = (liftIO . readIORef) . gmGhcSession =<< ask - setSession a = (liftIO . flip writeIORef a) . gmGhcSession =<< ask - -#if __GLASGOW_HASKELL__ >= 706 -instance (Functor m, MonadIO m, MonadBaseControl IO m) - => HasDynFlags (GhcModT m) where - getDynFlags = getSessionDynFlags -#endif - -instance (MonadIO m, MonadBaseControl IO m) - => ExceptionMonad (GhcModT m) where - gcatch act handler = control $ \run -> - run act `gcatch` (run . handler) - - gmask = liftBaseOp gmask . liftRestore - where liftRestore f r = f $ liftBaseOp_ r +runGhcModT'' :: IOish m + => GhcModEnv + -> GhcModState + -> GhcModT m a + -> m (Either GhcModError (a, GhcModState), GhcModLog) +runGhcModT'' r s a = do + flip runReaderT r $ runJournalT $ runErrorT $ runStateT (unGhcModT a) s diff --git a/Language/Haskell/GhcMod/Monad/Types.hs b/Language/Haskell/GhcMod/Monad/Types.hs new file mode 100644 index 0000000..cfcb29b --- /dev/null +++ b/Language/Haskell/GhcMod/Monad/Types.hs @@ -0,0 +1,442 @@ +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +{-# LANGUAGE CPP, GeneralizedNewtypeDeriving, FlexibleInstances #-} +{-# LANGUAGE FlexibleContexts, MultiParamTypeClasses, RankNTypes #-} +{-# LANGUAGE TypeFamilies, UndecidableInstances, BangPatterns #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Language.Haskell.GhcMod.Monad.Types ( + -- * Monad Types + GhcModT(..) + , GmlT(..) + , LightGhc(..) + , GmGhc + , IOish + -- * Environment, state and logging + , GhcModEnv(..) + , GhcModState(..) + , GhcModCaches(..) + , defaultGhcModState + , GmGhcSession(..) + , GmComponent(..) + , CompilerMode(..) + -- * Accessing 'GhcModEnv', 'GhcModState' and 'GhcModLog' + , GmLogLevel(..) + , GhcModLog(..) + , GhcModError(..) + , Gm + , GmEnv(..) + , GmState(..) + , GmLog(..) + , cradle + , options + , withOptions + , getCompilerMode + , setCompilerMode + -- * Re-exporting convenient stuff + , MonadIO + , liftIO + , gmlGetSession + , gmlSetSession + ) where + +-- MonadUtils of GHC 7.6 or earlier defines its own MonadIO. +-- RWST does not automatically become an instance of MonadIO. +-- MonadUtils of GHC 7.8 or later imports MonadIO in Monad.Control.IO.Class. +-- So, RWST automatically becomes an instance of +#if __GLASGOW_HASKELL__ < 708 +-- 'CoreMonad.MonadIO' and 'Control.Monad.IO.Class.MonadIO' are different +-- classes before ghc 7.8 +#define DIFFERENT_MONADIO 1 + +-- RWST doen't have a MonadIO instance before ghc 7.8 +#define MONADIO_INSTANCES 1 +#endif + +import Language.Haskell.GhcMod.Types + +import GHC +import DynFlags +import Exception +import HscTypes + +import Control.Applicative +import Control.Monad + +import Control.Monad.Reader (ReaderT(..)) +import Control.Monad.Error (ErrorT(..), MonadError(..)) +import Control.Monad.State.Strict (StateT(..)) +import Control.Monad.Trans.Journal (JournalT) +import Control.Monad.Trans.Maybe (MaybeT(..)) + +import Control.Monad.Base (MonadBase(..), liftBase) +import Control.Monad.Trans.Control + +import Control.Monad.Reader.Class +import Control.Monad.Writer.Class +import Control.Monad.State.Class (MonadState(..)) +import Control.Monad.Journal.Class (MonadJournal(..)) +import Control.Monad.Trans.Class (MonadTrans(..)) +import Control.Monad.Error (Error(..)) +import qualified Control.Monad.IO.Class as MTL + +#if DIFFERENT_MONADIO +import Data.Monoid (Monoid) +#endif + +import Data.Maybe +import Data.Monoid +import Data.IORef +import Prelude + +import qualified MonadUtils as GHC (MonadIO(..)) + +-- | This is basically a newtype wrapper around 'StateT', 'ErrorT', 'JournalT' +-- and 'ReaderT' with custom instances for 'GhcMonad' and it's constraints that +-- means you can run (almost) all functions from the GHC API on top of 'GhcModT' +-- transparently. +-- +-- The inner monad @m@ should have instances for 'MonadIO' and +-- 'MonadBaseControl' 'IO', in the common case this is simply 'IO'. Most @mtl@ +-- monads already have 'MonadBaseControl' 'IO' instances, see the +-- @monad-control@ package. +newtype GhcModT m a = GhcModT { + unGhcModT :: StateT GhcModState + (ErrorT GhcModError + (JournalT GhcModLog + (ReaderT GhcModEnv m) ) ) a + } deriving ( Functor + , Applicative + , Alternative + , Monad + , MonadPlus + , MTL.MonadIO +#if DIFFERENT_MONADIO + , GHC.MonadIO +#endif + , MonadError GhcModError + ) + +newtype GmlT m a = GmlT { unGmlT :: GhcModT m a } + deriving ( Functor + , Applicative + , Alternative + , Monad + , MonadPlus + , MonadTrans + , MTL.MonadIO +#if DIFFERENT_MONADIO + , GHC.MonadIO +#endif + , MonadError GhcModError + , GmEnv + , GmState + , GmLog + ) + +newtype LightGhc a = LightGhc { unLightGhc :: ReaderT (IORef HscEnv) IO a } + deriving ( Functor + , Applicative + , Monad + , MTL.MonadIO +#if DIFFERENT_MONADIO + , GHC.MonadIO +#endif + ) + +#if DIFFERENT_MONADIO +instance MTL.MonadIO m => GHC.MonadIO (ReaderT x m) where + liftIO = MTL.liftIO +instance MTL.MonadIO m => GHC.MonadIO (StateT x m) where + liftIO = MTL.liftIO +instance (Error e, MTL.MonadIO m) => GHC.MonadIO (ErrorT e m) where + liftIO = MTL.liftIO +instance MTL.MonadIO m => GHC.MonadIO (JournalT x m) where + liftIO = MTL.liftIO +instance MTL.MonadIO m => GHC.MonadIO (MaybeT m) where + liftIO = MTL.liftIO +#endif + +instance MonadIO IO where + liftIO = id +instance MonadIO m => MonadIO (ReaderT x m) where + liftIO = MTL.liftIO +instance MonadIO m => MonadIO (StateT x m) where + liftIO = MTL.liftIO +instance (Error e, MonadIO m) => MonadIO (ErrorT e m) where + liftIO = MTL.liftIO +instance MonadIO m => MonadIO (JournalT x m) where + liftIO = MTL.liftIO +instance MonadIO m => MonadIO (MaybeT m) where + liftIO = MTL.liftIO +instance MonadIOC m => MonadIO (GhcModT m) where + liftIO = MTL.liftIO +instance MonadIOC m => MonadIO (GmlT m) where + liftIO = MTL.liftIO +instance MonadIO LightGhc where + liftIO = MTL.liftIO + +class Monad m => GmEnv m where + gmeAsk :: m GhcModEnv + gmeAsk = gmeReader id + + gmeReader :: (GhcModEnv -> a) -> m a + gmeReader f = f `liftM` gmeAsk + + gmeLocal :: (GhcModEnv -> GhcModEnv) -> m a -> m a + {-# MINIMAL (gmeAsk | gmeReader), gmeLocal #-} + +type Gm m = (GmEnv m, GmState m, GmLog m) + +instance Monad m => GmEnv (GhcModT m) where + gmeAsk = GhcModT ask + gmeReader = GhcModT . reader + gmeLocal f a = GhcModT $ local f (unGhcModT a) + +instance GmEnv m => GmEnv (StateT s m) where + gmeAsk = lift gmeAsk + gmeReader = lift . gmeReader + gmeLocal f (StateT a) = StateT $ \s -> gmeLocal f (a s) + +class Monad m => GmState m where + gmsGet :: m GhcModState + gmsGet = gmsState (\s -> (s, s)) + + gmsPut :: GhcModState -> m () + gmsPut s = gmsState (\_ -> ((), s)) + + gmsState :: (GhcModState -> (a, GhcModState)) -> m a + gmsState f = do + s <- gmsGet + let ~(a, s') = f s + gmsPut s' + return a + {-# MINIMAL gmsState | gmsGet, gmsPut #-} + +instance Monad m => GmState (StateT GhcModState m) where + gmsGet = get + gmsPut = put + gmsState = state + +instance Monad m => GmState (GhcModT m) where + gmsGet = GhcModT get + gmsPut = GhcModT . put + gmsState = GhcModT . state + +instance GmState m => GmState (MaybeT m) where + gmsGet = MaybeT $ Just `liftM` gmsGet + gmsPut = MaybeT . (Just `liftM`) . gmsPut + gmsState = MaybeT . (Just `liftM`) . gmsState + +class Monad m => GmLog m where + gmlJournal :: GhcModLog -> m () + gmlHistory :: m GhcModLog + gmlClear :: m () + +instance Monad m => GmLog (JournalT GhcModLog m) where + gmlJournal = journal + gmlHistory = history + gmlClear = clear + +instance Monad m => GmLog (GhcModT m) where + gmlJournal = GhcModT . lift . lift . journal + gmlHistory = GhcModT $ lift $ lift history + gmlClear = GhcModT $ lift $ lift clear + +instance (Monad m, GmLog m) => GmLog (ReaderT r m) where + gmlJournal = lift . gmlJournal + gmlHistory = lift gmlHistory + gmlClear = lift gmlClear + +instance (Monad m, GmLog m) => GmLog (StateT s m) where + gmlJournal = lift . gmlJournal + gmlHistory = lift gmlHistory + gmlClear = lift gmlClear + +instance Monad m => MonadJournal GhcModLog (GhcModT m) where + journal !w = GhcModT $ lift $ lift $ (journal w) + history = GhcModT $ lift $ lift $ history + clear = GhcModT $ lift $ lift $ clear + +instance MonadTrans GhcModT where + lift = GhcModT . lift . lift . lift . lift + +instance forall r m. MonadReader r m => MonadReader r (GhcModT m) where + local f ma = gmLiftWithInner (\run -> local f (run ma)) + ask = gmLiftInner ask + +instance (Monoid w, MonadWriter w m) => MonadWriter w (GhcModT m) where + tell = gmLiftInner . tell + listen ma = + liftWith (\run -> listen (run ma)) >>= \(sta, w) -> + flip (,) w `liftM` restoreT (return sta) + + pass maww = maww >>= gmLiftInner . pass . return + +instance MonadState s m => MonadState s (GhcModT m) where + get = GhcModT $ lift $ lift $ lift get + put = GhcModT . lift . lift . lift . put + state = GhcModT . lift . lift . lift . state + +instance (MonadBaseControl IO m) => MonadBase IO (GmlT m) where + liftBase = GmlT . liftBase + +instance (MonadBaseControl IO m) => MonadBaseControl IO (GmlT m) where + type StM (GmlT m) a = StM (GhcModT m) a + liftBaseWith = defaultLiftBaseWith + restoreM = defaultRestoreM + {-# INLINE liftBaseWith #-} + {-# INLINE restoreM #-} + +instance MonadTransControl GmlT where + type StT GmlT a = StT GhcModT a + liftWith = defaultLiftWith GmlT unGmlT + restoreT = defaultRestoreT GmlT + +instance (MonadBaseControl IO m) => MonadBase IO (GhcModT m) where + liftBase = GhcModT . liftBase + +instance (MonadBaseControl IO m) => MonadBaseControl IO (GhcModT m) where + type StM (GhcModT m) a = + StM (StateT GhcModState + (ErrorT GhcModError + (JournalT GhcModLog + (ReaderT GhcModEnv m) ) ) ) a + + liftBaseWith f = GhcModT (liftBaseWith $ \runInBase -> + f $ runInBase . unGhcModT) + + restoreM = GhcModT . restoreM + {-# INLINE liftBaseWith #-} + {-# INLINE restoreM #-} + +instance MonadTransControl GhcModT where + type StT GhcModT a = (Either GhcModError (a, GhcModState), GhcModLog) + + liftWith f = GhcModT $ + liftWith $ \runS -> + liftWith $ \runE -> + liftWith $ \runJ -> + liftWith $ \runR -> + f $ \ma -> runR $ runJ $ runE $ runS $ unGhcModT ma + restoreT = GhcModT . restoreT . restoreT . restoreT . restoreT + {-# INLINE liftWith #-} + {-# INLINE restoreT #-} + +gmLiftInner :: Monad m => m a -> GhcModT m a +gmLiftInner = GhcModT . lift . lift . lift . lift + +gmLiftWithInner :: (MonadTransControl t, Monad m, Monad (t m)) + => (Run t -> m (StT t a)) -> t m a +gmLiftWithInner f = liftWith f >>= restoreT . return + +-- GHC cannot prove the following instances to be decidable automatically using +-- the FlexibleContexts extension as they violate the second Paterson Condition, +-- namely that: The assertion has fewer constructors and variables (taken +-- together and counting repetitions) than the head. Specifically the +-- @MonadBaseControl IO m@ constraint in 'IOish' is causing this violation. + +type GmGhc m = (IOish m, GhcMonad m) + +instance (MonadIO m, MonadBaseControl IO m) => GhcMonad (GmlT m) where + getSession = gmlGetSession + setSession = gmlSetSession + +-- --------------------------------------------------------------------- + +gmlGetSession :: (MonadIO m, MonadBaseControl IO m) => GmlT m HscEnv +gmlGetSession = do + ref <- gmgsSession . fromJust . gmGhcSession <$> gmsGet + GHC.liftIO $ readIORef ref + +gmlSetSession :: (MonadIO m, MonadBaseControl IO m) => HscEnv -> GmlT m () +gmlSetSession a = do + ref <- gmgsSession . fromJust . gmGhcSession <$> gmsGet + GHC.liftIO $ flip writeIORef a ref + +-- --------------------------------------------------------------------- +instance GhcMonad LightGhc where + getSession = (GHC.liftIO . readIORef) =<< LightGhc ask + setSession a = (GHC.liftIO . flip writeIORef a) =<< LightGhc ask + +#if __GLASGOW_HASKELL__ >= 706 +instance (MonadIO m, MonadBaseControl IO m) => HasDynFlags (GmlT m) where + getDynFlags = hsc_dflags <$> getSession + +instance HasDynFlags LightGhc where + getDynFlags = hsc_dflags <$> getSession +#endif + +instance (MonadIO m, MonadBaseControl IO m) => ExceptionMonad (GhcModT m) where + gcatch act handler = control $ \run -> + run act `gcatch` (run . handler) + + gmask = liftBaseOp gmask . liftRestore + where liftRestore f r = f $ liftBaseOp_ r + +instance (MonadIO m, MonadBaseControl IO m) => ExceptionMonad (GmlT m) where + gcatch act handler = control $ \run -> + run act `gcatch` (run . handler) + + gmask = liftBaseOp gmask . liftRestore + where liftRestore f r = f $ liftBaseOp_ r + +instance ExceptionMonad LightGhc where + gcatch act handl = + LightGhc $ unLightGhc act `gcatch` \e -> unLightGhc (handl e) + gmask f = + LightGhc $ gmask $ \io_restore ->let + g_restore (LightGhc m) = LightGhc $ io_restore m + in + unLightGhc (f g_restore) + + +instance (MonadIO m, MonadBaseControl IO m) => ExceptionMonad (StateT s m) where + gcatch act handler = control $ \run -> + run act `gcatch` (run . handler) + + gmask = liftBaseOp gmask . liftRestore + where liftRestore f r = f $ liftBaseOp_ r + +instance (MonadIO m, MonadBaseControl IO m) => ExceptionMonad (ReaderT s m) where + gcatch act handler = control $ \run -> + run act `gcatch` (run . handler) + + gmask = liftBaseOp gmask . liftRestore + where liftRestore f r = f $ liftBaseOp_ r + +---------------------------------------------------------------- + +options :: GmEnv m => m Options +options = gmOptions `liftM` gmeAsk + +cradle :: GmEnv m => m Cradle +cradle = gmCradle `liftM` gmeAsk + +getCompilerMode :: GmState m => m CompilerMode +getCompilerMode = gmCompilerMode `liftM` gmsGet + +setCompilerMode :: GmState m => CompilerMode -> m () +setCompilerMode mode = (\s -> gmsPut s { gmCompilerMode = mode } ) =<< gmsGet + +withOptions :: GmEnv m => (Options -> Options) -> m a -> m a +withOptions changeOpt action = gmeLocal changeEnv action + where + changeEnv e = e { gmOptions = changeOpt opt } + where + opt = gmOptions e diff --git a/Language/Haskell/GhcMod/Output.hs b/Language/Haskell/GhcMod/Output.hs new file mode 100644 index 0000000..e96956a --- /dev/null +++ b/Language/Haskell/GhcMod/Output.hs @@ -0,0 +1,199 @@ +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +-- Derived from process:System.Process +-- Copyright (c) The University of Glasgow 2004-2008 + +module Language.Haskell.GhcMod.Output ( + gmPutStr + , gmErrStr + , gmPutStrLn + , gmErrStrLn + , gmUnsafePutStrLn + , gmUnsafeErrStrLn + , gmReadProcess + , stdoutGateway + ) where + +import Data.List +import System.IO +import System.Exit +import System.Process +import Control.Monad +import Control.DeepSeq +import Control.Exception +import Control.Concurrent + +import Language.Haskell.GhcMod.Types hiding (LineSeparator) +import Language.Haskell.GhcMod.Monad.Types + +withLines :: (String -> String) -> String -> String +withLines f s = let + res = unlines $ map f $ lines s + in + case s of + [] -> res + _ | not $ isTerminated s -> + reverse $ drop 1 $ reverse res + _ -> res + +isTerminated :: String -> Bool +isTerminated "" = False +isTerminated s = isNewline (last s) + +isNewline :: Char -> Bool +isNewline c = c == '\n' + +toGmLines :: String -> (GmLines String) +toGmLines "" = GmLines GmPartial "" +toGmLines s | isNewline (last s) = GmLines GmTerminated s +toGmLines s = GmLines GmPartial s + +outputFns :: (GmEnv m, MonadIO m') + => m (GmLines String -> m' (), GmLines String -> m' ()) +outputFns = do + opts <- options + env <- gmeAsk + return $ outputFns' opts (gmOutput env) + +outputFns' :: MonadIO m' + => Options + -> GmOutput + -> (GmLines String -> m' (), GmLines String -> m' ()) +outputFns' opts output = let + Options {..} = opts + + pfx f = withLines f + + outPfx, errPfx :: GmLines String -> GmLines String + (outPfx, errPfx) = + case linePrefix of + Nothing -> ( id, id ) + Just (op, ep) -> ( fmap $ pfx (op++), fmap $ pfx (ep++) ) + in + case output of + GmOutputStdio -> + ( liftIO . putStr . unGmLine . outPfx + , liftIO . hPutStr stderr . unGmLine . errPfx) + GmOutputChan c -> + ( liftIO . writeChan c . (,) GmOut . outPfx + , liftIO . writeChan c . (,) GmErr .errPfx) + +gmPutStr, gmPutStrLn, gmErrStr, gmErrStrLn + :: (MonadIO m, GmEnv m) => String -> m () + +gmPutStr str = do + putOut <- fst `liftM` outputFns + putOut $ toGmLines str + +gmPutStrLn = gmPutStr . (++"\n") +gmErrStrLn = gmErrStr . (++"\n") + +gmErrStr str = do + putErr <- snd `liftM` outputFns + putErr $ toGmLines str + +-- | Only use these when you're sure there are no other writers on stdout +gmUnsafePutStrLn, gmUnsafeErrStrLn + :: MonadIO m => Options -> String -> m () +gmUnsafePutStrLn opts = (fst $ outputFns' opts GmOutputStdio) . toGmLines +gmUnsafeErrStrLn opts = (snd $ outputFns' opts GmOutputStdio) . toGmLines + +gmReadProcess :: GmEnv m => m (FilePath -> [String] -> String -> IO String) +gmReadProcess = do + GhcModEnv {..} <- gmeAsk + case gmOutput of + GmOutputChan _ -> + readProcessStderrChan + GmOutputStdio -> + return $ readProcess + +stdoutGateway :: Chan (GmStream, GmLines String) -> IO () +stdoutGateway chan = go ("", "") + where + go buf@(obuf, ebuf) = do + (stream, GmLines ty l) <- readChan chan + case ty of + GmTerminated -> + case stream of + GmOut -> putStr (obuf++l) >> go ("", ebuf) + GmErr -> putStr (ebuf++l) >> go (obuf, "") + GmPartial -> case reverse $ lines l of + [] -> go buf + [x] -> go (appendBuf stream buf x) + x:xs -> do + putStr $ unlines $ reverse xs + go (appendBuf stream buf x) + + appendBuf GmOut (obuf, ebuf) s = (obuf++s, ebuf) + appendBuf GmErr (obuf, ebuf) s = (obuf, ebuf++s) + + +readProcessStderrChan :: + GmEnv m => m (FilePath -> [String] -> String -> IO String) +readProcessStderrChan = do + (_, e) <- outputFns + return $ go e + where + go :: (GmLines String -> IO ()) -> FilePath -> [String] -> String -> IO String + go putErr exe args input = do + let cp = (proc exe args) { + std_out = CreatePipe + , std_err = CreatePipe + , std_in = CreatePipe + } + (Just i, Just o, Just e, h) <- createProcess cp + + _ <- forkIO $ reader e + + output <- hGetContents o + withForkWait (evaluate $ rnf output) $ \waitOut -> do + + -- now write any input + unless (null input) $ + ignoreSEx $ hPutStr i input + -- hClose performs implicit hFlush, and thus may trigger a SIGPIPE + ignoreSEx $ hClose i + + -- wait on the output + waitOut + hClose o + + res <- waitForProcess h + case res of + ExitFailure rv -> + processFailedException "readProcessStderrChan" exe args rv + ExitSuccess -> + return output + where + ignoreSEx = handle (\(SomeException _) -> return ()) + reader h = ignoreSEx $ do + putErr . toGmLines . (++"\n") =<< hGetLine h + reader h + +withForkWait :: IO () -> (IO () -> IO a) -> IO a +withForkWait async body = do + waitVar <- newEmptyMVar :: IO (MVar (Either SomeException ())) + mask $ \restore -> do + tid <- forkIO $ try (restore async) >>= putMVar waitVar + let wait = takeMVar waitVar >>= either throwIO return + restore (body wait) `onException` killThread tid + +processFailedException :: String -> String -> [String] -> Int -> IO a +processFailedException fn exe args rv = + error $ concat [ fn, ": ", exe, " " + , intercalate " " (map show args) + , " (exit " ++ show rv ++ ")"] diff --git a/Language/Haskell/GhcMod/PathsAndFiles.hs b/Language/Haskell/GhcMod/PathsAndFiles.hs index 064d39e..085cf52 100644 --- a/Language/Haskell/GhcMod/PathsAndFiles.hs +++ b/Language/Haskell/GhcMod/PathsAndFiles.hs @@ -1,21 +1,42 @@ -{-# LANGUAGE BangPatterns, TupleSections #-} -module Language.Haskell.GhcMod.PathsAndFiles where +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . +module Language.Haskell.GhcMod.PathsAndFiles ( + module Language.Haskell.GhcMod.PathsAndFiles + , module Language.Haskell.GhcMod.Caching + ) where + +import Config (cProjectVersion) import Control.Applicative import Control.Monad import Data.List import Data.Char import Data.Maybe -import Data.Traversable (traverse) -import Language.Haskell.GhcMod.Types +import Data.Traversable hiding (mapM) +import Distribution.Helper (buildPlatform) import System.Directory import System.FilePath +import System.Process +import Language.Haskell.GhcMod.Types import Language.Haskell.GhcMod.Error +import Language.Haskell.GhcMod.Caching import qualified Language.Haskell.GhcMod.Utils as U - -import Distribution.Simple.BuildPaths (defaultDistPref) -import Distribution.Simple.Configure (localBuildInfoFile) +import Utils (mightExist) +import Prelude -- | Guaranteed to be a path to a directory with no trailing slash. type DirPath = FilePath @@ -23,40 +44,111 @@ type DirPath = FilePath -- | Guaranteed to be the name of a file only (no slashes). type FileName = String +newtype UnString = UnString { unString :: String } + +instance Show UnString where + show = unString + +instance Read UnString where + readsPrec _ = \str -> [(UnString str, "")] + -- | @findCabalFiles dir@. Searches for a @.cabal@ files in @dir@'s parent -- directories. The first parent directory containing more than one cabal file -- is assumed to be the project directory. If only one cabal file exists in this -- directory it is returned otherwise @findCabalFiles@ throws 'GMENoCabalFile' -- or 'GMETooManyCabalFiles' findCabalFile :: FilePath -> IO (Maybe FilePath) -findCabalFile directory = do - -- Look for cabal files in @dir@ and all it's parent directories - dcs <- getCabalFiles `zipMapM` parents directory - -- Extract first non-empty list, which represents a directory with cabal - -- files. - case find (not . null) $ uncurry appendDir `map` dcs of - Just [] -> throw $ GMENoCabalFile +findCabalFile dir = do + -- List of directories and all cabal file candidates + dcs <- findFileInParentsP isCabalFile dir :: IO ([(DirPath, [FileName])]) + let css = uncurry appendDir `map` dcs :: [[FilePath]] + case find (not . null) css of + Nothing -> return Nothing Just cfs@(_:_:_) -> throw $ GMETooManyCabalFiles cfs - a -> return $ head <$> a + Just (a:_) -> return (Just a) + Just [] -> error "findCabalFile" where appendDir :: DirPath -> [FileName] -> [FilePath] - appendDir dir fs = (dir ) `map` fs + appendDir d fs = (d ) `map` fs + +-- | Get path to sandbox config file +getSandboxDb :: FilePath + -- ^ Path to the cabal package root directory (containing the + -- @cabal.sandbox.config@ file) + -> IO (Maybe GhcPkgDb) +getSandboxDb d = do + mConf <- traverse readFile =<< mightExist (d "cabal.sandbox.config") + bp <- buildPlatform readProcess + return $ PackageDb . fixPkgDbVer bp <$> (extractSandboxDbDir =<< mConf) --- | @getCabalFiles dir@. Find all files ending in @.cabal@ in @dir@. -getCabalFiles :: DirPath -> IO [FileName] -getCabalFiles dir = - filterM isCabalFile =<< getDirectoryContents dir where - isCabalFile f = do - exists <- doesFileExist $ dir f - return (exists && takeExtension' f == ".cabal") + fixPkgDbVer bp dir = + case takeFileName dir == ghcSandboxPkgDbDir bp of + True -> dir + False -> takeDirectory dir ghcSandboxPkgDbDir bp - takeExtension' p = if takeFileName p == takeExtension p - then "" - else takeExtension p +-- | Extract the sandbox package db directory from the cabal.sandbox.config +-- file. Exception is thrown if the sandbox config file is broken. +extractSandboxDbDir :: String -> Maybe FilePath +extractSandboxDbDir conf = extractValue <$> parse conf + where + key = "package-db:" + keyLen = length key + + parse = listToMaybe . filter (key `isPrefixOf`) . lines + extractValue = U.dropWhileEnd isSpace . dropWhile isSpace . drop keyLen + + +-- | +-- >>> isCabalFile "/home/user/.cabal" +-- False +isCabalFile :: FilePath -> Bool +isCabalFile f = takeExtension' f == ".cabal" + +-- | +-- >>> takeExtension' "/some/dir/bla.cabal" +-- ".cabal" +-- +-- >>> takeExtension' "some/reldir/bla.cabal" +-- ".cabal" +-- +-- >>> takeExtension' "bla.cabal" +-- ".cabal" +-- +-- >>> takeExtension' ".cabal" +-- "" +takeExtension' :: FilePath -> String +takeExtension' p = + if takeFileName p == takeExtension p + then "" -- just ".cabal" is not a valid cabal file + else takeExtension p + +-- | @findFileInParentsP p dir@ Look for files satisfying @p@ in @dir@ and all +-- it's parent directories. +findFileInParentsP :: (FilePath -> Bool) -> FilePath + -> IO [(DirPath, [FileName])] +findFileInParentsP p dir = + getFilesP p `zipMapM` parents dir + +-- | @getFilesP p dir@. Find all __files__ satisfying @p@ in @.cabal@ in @dir@. +getFilesP :: (FilePath -> Bool) -> DirPath -> IO [FileName] +getFilesP p dir = filterM p' =<< getDirectoryContents dir + where + p' fn = do + (p fn && ) <$> doesFileExist (dir fn) + +findCabalSandboxDir :: FilePath -> IO (Maybe FilePath) +findCabalSandboxDir dir = do + dss <- findFileInParentsP isSandboxConfig dir + return $ case find (not . null . snd) $ dss of + Just (sbDir, _:_) -> Just sbDir + _ -> Nothing + + where + isSandboxConfig = (==sandboxConfigFile) zipMapM :: Monad m => (a -> m c) -> [a] -> m [(a,c)] -zipMapM f as = mapM (\a -> liftM (a,) $ f a) as +zipMapM f as = mapM (\a -> liftM ((,) a) $ f a) as -- | @parents dir@. Returns all parent directories of @dir@ including @dir@. -- @@ -86,31 +178,48 @@ parents dir' = ---------------------------------------------------------------- --- | Get path to sandbox config file -getSandboxDb :: FilePath -- ^ Path to the cabal package root directory - -- (containing the @cabal.sandbox.config@ file) - -> IO (Maybe FilePath) -getSandboxDb d = do - mConf <- traverse readFile =<< U.mightExist (d "cabal.sandbox.config") - return $ extractSandboxDbDir =<< mConf - --- | Extract the sandbox package db directory from the cabal.sandbox.config file. --- Exception is thrown if the sandbox config file is broken. -extractSandboxDbDir :: String -> Maybe FilePath -extractSandboxDbDir conf = extractValue <$> parse conf - where - key = "package-db:" - keyLen = length key - - parse = listToMaybe . filter (key `isPrefixOf`) . lines - extractValue = U.dropWhileEnd isSpace . dropWhile isSpace . drop keyLen - setupConfigFile :: Cradle -> FilePath setupConfigFile crdl = cradleRootDir crdl setupConfigPath +sandboxConfigFile :: FilePath +sandboxConfigFile = "cabal.sandbox.config" + -- | Path to 'LocalBuildInfo' file, usually @dist/setup-config@ setupConfigPath :: FilePath -setupConfigPath = localBuildInfoFile defaultDistPref +setupConfigPath = "dist/setup-config" -- localBuildInfoFile defaultDistPref + +macrosHeaderPath :: FilePath +macrosHeaderPath = "dist/build/autogen/cabal_macros.h" + +ghcSandboxPkgDbDir :: String -> String +ghcSandboxPkgDbDir buildPlatf = do + buildPlatf ++ "-ghc-" ++ cProjectVersion ++ "-packages.conf.d" packageCache :: String packageCache = "package.cache" + +-- | Filename of the symbol table cache file. +symbolCache :: Cradle -> FilePath +symbolCache crdl = cradleTempDir crdl symbolCacheFile + +symbolCacheFile :: String +symbolCacheFile = "ghc-mod.symbol-cache" + +resolvedComponentsCacheFile :: String +resolvedComponentsCacheFile = setupConfigPath <.> "ghc-mod.resolved-components" + +cabalHelperCacheFile :: String +cabalHelperCacheFile = setupConfigPath <.> "ghc-mod.cabal-components" + +mergedPkgOptsCacheFile :: String +mergedPkgOptsCacheFile = setupConfigPath <.> "ghc-mod.package-options" + +pkgDbStackCacheFile :: String +pkgDbStackCacheFile = setupConfigPath <.> "ghc-mod.package-db-stack" + +-- | @findCustomPackageDbFile dir@. Searches for a @.ghc-mod.cradle@ file in @dir@. +-- If it exists in the given directory it is returned otherwise @findCradleFile@ returns @Nothing@ +findCustomPackageDbFile :: FilePath -> IO (Maybe FilePath) +findCustomPackageDbFile directory = do + let path = directory "ghc-mod.package-db-stack" + mightExist path diff --git a/Language/Haskell/GhcMod/PkgDoc.hs b/Language/Haskell/GhcMod/PkgDoc.hs index d981ddd..b469f87 100644 --- a/Language/Haskell/GhcMod/PkgDoc.hs +++ b/Language/Haskell/GhcMod/PkgDoc.hs @@ -5,22 +5,23 @@ import Language.Haskell.GhcMod.GhcPkg import Language.Haskell.GhcMod.Monad import Language.Haskell.GhcMod.Utils -import Control.Applicative ((<$>)) +import Control.Applicative +import Prelude -- | Obtaining the package name and the doc path of a module. pkgDoc :: IOish m => String -> GhcModT m String pkgDoc mdl = do - c <- cradle - pkg <- trim <$> readProcess' "ghc-pkg" (toModuleOpts c) + pkgDbStack <- getPackageDbStack + pkg <- liftIO $ trim <$> readProcess "ghc-pkg" (toModuleOpts pkgDbStack) "" if pkg == "" then return "\n" else do - htmlpath <- readProcess' "ghc-pkg" (toDocDirOpts pkg c) + htmlpath <- liftIO $ readProcess "ghc-pkg" (toDocDirOpts pkg pkgDbStack) "" let ret = pkg ++ " " ++ drop 14 htmlpath return ret where - toModuleOpts c = ["find-module", mdl, "--simple-output"] - ++ ghcPkgDbStackOpts (cradlePkgDbStack c) - toDocDirOpts pkg c = ["field", pkg, "haddock-html"] - ++ ghcPkgDbStackOpts (cradlePkgDbStack c) + toModuleOpts dbs = ["find-module", mdl, "--simple-output"] + ++ ghcPkgDbStackOpts dbs + toDocDirOpts pkg dbs = ["field", pkg, "haddock-html"] + ++ ghcPkgDbStackOpts dbs trim = takeWhile (`notElem` " \n") diff --git a/Language/Haskell/GhcMod/Pretty.hs b/Language/Haskell/GhcMod/Pretty.hs new file mode 100644 index 0000000..5526772 --- /dev/null +++ b/Language/Haskell/GhcMod/Pretty.hs @@ -0,0 +1,69 @@ +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +module Language.Haskell.GhcMod.Pretty where + +import Control.Arrow hiding ((<+>)) +import Data.Char +import Data.List +import Distribution.Helper +import Text.PrettyPrint + +import Language.Haskell.GhcMod.Types + +docStyle :: Style +docStyle = style { ribbonsPerLine = 1.2 } + +gmRenderDoc :: Doc -> String +gmRenderDoc = renderStyle docStyle + +gmComponentNameDoc :: ChComponentName -> Doc +gmComponentNameDoc ChSetupHsName = text $ "Setup.hs" +gmComponentNameDoc ChLibName = text $ "library" +gmComponentNameDoc (ChExeName n) = text $ "exe:" ++ n +gmComponentNameDoc (ChTestName n) = text $ "test:" ++ n +gmComponentNameDoc (ChBenchName n) = text $ "bench:" ++ n + +gmLogLevelDoc :: GmLogLevel -> Doc +gmLogLevelDoc GmSilent = error "GmSilent MUST not be used for log messages" +gmLogLevelDoc GmPanic = text "PANIC" +gmLogLevelDoc GmException = text "EXCEPTION" +gmLogLevelDoc GmError = text "ERROR" +gmLogLevelDoc GmWarning = text "Warning" +gmLogLevelDoc GmInfo = text "info" +gmLogLevelDoc GmDebug = text "DEBUG" +gmLogLevelDoc GmVomit = text "VOMIT" + +infixl 6 <+>: +(<+>:) :: Doc -> Doc -> Doc +a <+>: b = (a <> colon) <+> b + +fnDoc :: FilePath -> Doc +fnDoc = doubleQuotes . text + +showDoc :: Show a => a -> Doc +showDoc = text . show + +warnDoc :: Doc -> Doc +warnDoc d = text "Warning" <+>: d + +strDoc :: String -> Doc +strDoc str = doc (dropWhileEnd isSpace str) + where + doc :: String -> Doc + doc = lines + >>> map (words >>> map text >>> fsep) + >>> \l -> case l of (x:xs) -> hang x 4 (vcat xs); [] -> empty diff --git a/Language/Haskell/GhcMod/SrcUtils.hs b/Language/Haskell/GhcMod/SrcUtils.hs index 87b4840..0938f81 100644 --- a/Language/Haskell/GhcMod/SrcUtils.hs +++ b/Language/Haskell/GhcMod/SrcUtils.hs @@ -3,7 +3,7 @@ module Language.Haskell.GhcMod.SrcUtils where -import Control.Applicative ((<$>)) +import Control.Applicative import CoreUtils (exprType) import Data.Generics import Data.Maybe (fromMaybe) @@ -13,15 +13,13 @@ import qualified GHC as G import GHC.SYB.Utils (Stage(..), everythingStaged) import GhcMonad import qualified Language.Haskell.Exts.Annotated as HE -import Language.Haskell.GhcMod.Doc (showOneLine, getStyle) -import Language.Haskell.GhcMod.DynFlags -import Language.Haskell.GhcMod.Gap (HasType(..), setWarnTypedHoles, setDeferTypeErrors) +import Language.Haskell.GhcMod.Doc +import Language.Haskell.GhcMod.Gap import qualified Language.Haskell.GhcMod.Gap as Gap -import Language.Haskell.GhcMod.Monad (IOish, GhcModT) -import Language.Haskell.GhcMod.Target (setTargetFiles) import OccName (OccName) import Outputable (PprStyle) import TcHsSyn (hsPatType) +import Prelude ---------------------------------------------------------------- @@ -83,22 +81,6 @@ typeSigInRangeHE _ _ _= False pretty :: DynFlags -> PprStyle -> Type -> String pretty dflag style = showOneLine dflag style . Gap.typeForUser ----------------------------------------------------------------- - -inModuleContext :: IOish m - => FilePath - -> (DynFlags -> PprStyle -> GhcModT m a) - -> GhcModT m a -inModuleContext file action = - withDynFlags (setWarnTypedHoles . setDeferTypeErrors . setNoWarningFlags) $ do - setTargetFiles [file] - Gap.withContext $ do - dflag <- G.getSessionDynFlags - style <- getStyle - action dflag style - ----------------------------------------------------------------- - showName :: DynFlags -> PprStyle -> G.Name -> String showName dflag style name = showOneLine dflag style $ Gap.nameForUser name diff --git a/Language/Haskell/GhcMod/Target.hs b/Language/Haskell/GhcMod/Target.hs index 51d64e4..c02d38e 100644 --- a/Language/Haskell/GhcMod/Target.hs +++ b/Language/Haskell/GhcMod/Target.hs @@ -1,60 +1,486 @@ -{-# LANGUAGE CPP #-} -module Language.Haskell.GhcMod.Target ( - setTargetFiles - ) where +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +{-# LANGUAGE CPP, ViewPatterns, NamedFieldPuns, RankNTypes #-} +module Language.Haskell.GhcMod.Target where + +import Control.Arrow +import Control.Applicative +import Control.Category ((.)) +import Control.Monad.Reader (runReaderT) +import GHC +import GHC.Paths (libdir) +import StaticFlags +import SysTools +import DynFlags +import HscMain +import HscTypes -import Control.Applicative ((<$>)) -import Control.Monad (forM, void, (>=>)) -import DynFlags (ExtensionFlag(..), xopt) -import GHC (LoadHowMuch(..)) -import qualified GHC as G import Language.Haskell.GhcMod.DynFlags -import Language.Haskell.GhcMod.Monad +import Language.Haskell.GhcMod.Monad.Types +import Language.Haskell.GhcMod.CabalHelper +import Language.Haskell.GhcMod.HomeModuleGraph +import Language.Haskell.GhcMod.PathsAndFiles +import Language.Haskell.GhcMod.GhcPkg +import Language.Haskell.GhcMod.Error +import Language.Haskell.GhcMod.Logging +import Language.Haskell.GhcMod.Types +import Language.Haskell.GhcMod.Utils as U + + +import Data.Maybe +import Data.Monoid as Monoid +import Data.Either +import Data.Foldable as Foldable (foldrM) +import qualified Data.Foldable as Foldable +import Data.Traversable hiding (mapM, forM) +import Data.IORef +import Data.List +import Data.Map (Map) +import qualified Data.Map as Map +import Data.Set (Set) +import qualified Data.Set as Set +import Distribution.Helper +import Prelude hiding ((.)) + +import System.Directory +import System.FilePath + +withLightHscEnv :: forall m a. IOish m + => [GHCOption] -> (HscEnv -> m a) -> m a +withLightHscEnv opts action = gbracket initEnv teardownEnv action + where + teardownEnv :: HscEnv -> m () + teardownEnv env = liftIO $ do + let dflags = hsc_dflags env + cleanTempFiles dflags + cleanTempDirs dflags + + initEnv :: m HscEnv + initEnv = liftIO $ do + initStaticOpts + settings <- initSysTools (Just libdir) + dflags <- initDynFlags (defaultDynFlags settings) + env <- newHscEnv dflags + dflags' <- runLightGhc env $ do + -- HomeModuleGraph and probably all other clients get into all sorts of + -- trouble if the package state isn't initialized here + _ <- setSessionDynFlags =<< addCmdOpts opts =<< getSessionDynFlags + getSessionDynFlags + newHscEnv dflags' + +runLightGhc :: HscEnv -> LightGhc a -> IO a +runLightGhc env action = do + renv <- newIORef env + flip runReaderT renv $ unLightGhc action + +runGmPkgGhc :: (IOish m, GmEnv m, GmState m, GmLog m) => LightGhc a -> m a +runGmPkgGhc action = do + pkgOpts <- packageGhcOptions + withLightHscEnv pkgOpts $ \env -> liftIO $ runLightGhc env action + +initSession :: IOish m + => [GHCOption] -> (DynFlags -> Ghc DynFlags) -> GhcModT m () +initSession opts mdf = do + s <- gmsGet + case gmGhcSession s of + Just GmGhcSession {..} -> when (gmgsOptions /= opts) $ putNewSession s + Nothing -> putNewSession s + + where + putNewSession s = do + rghc <- (liftIO . newIORef =<< newSession =<< cradle) + gmsPut s { gmGhcSession = Just $ GmGhcSession opts rghc } + + newSession Cradle { cradleTempDir } = liftIO $ do + runGhc (Just libdir) $ do + let setDf df = setTmpDir cradleTempDir <$> (mdf =<< addCmdOpts opts df) + _ <- setSessionDynFlags =<< setDf =<< getSessionDynFlags + getSession + +-- | Drop the currently active GHC session, the next that requires a GHC session +-- will initialize a new one. +dropSession :: IOish m => GhcModT m () +dropSession = do + s <- gmsGet + case gmGhcSession s of + Just (GmGhcSession _opts ref) -> do + -- TODO: This is still not enough, there seem to still be references to + -- GHC's state around afterwards. + liftIO $ writeIORef ref (error "HscEnv: session was dropped") + -- Not available on ghc<7.8; didn't really help anyways + -- liftIO $ setUnsafeGlobalDynFlags (error "DynFlags: session was dropped") + gmsPut s { gmGhcSession = Nothing } + + Nothing -> return () + + +runGmlT :: IOish m => [Either FilePath ModuleName] -> GmlT m a -> GhcModT m a +runGmlT fns action = runGmlT' fns return action + +runGmlT' :: IOish m + => [Either FilePath ModuleName] + -> (DynFlags -> Ghc DynFlags) + -> GmlT m a + -> GhcModT m a +runGmlT' fns mdf action = runGmlTWith fns mdf id action + +runGmlTWith :: IOish m + => [Either FilePath ModuleName] + -> (DynFlags -> Ghc DynFlags) + -> (GmlT m a -> GmlT m b) + -> GmlT m a + -> GhcModT m b +runGmlTWith efnmns' mdf wrapper action = do + crdl <- cradle + Options { ghcUserOptions } <- options + + let (fns, mns) = partitionEithers efnmns' + ccfns = map (cradleCurrentDir crdl ) fns + cfns <- liftIO $ mapM canonicalizePath ccfns + let serfnmn = Set.fromList $ map Right mns ++ map Left cfns + opts <- targetGhcOptions crdl serfnmn + let opts' = opts ++ ["-O0"] ++ ghcUserOptions + + gmVomit + "session-ghc-options" + (text "Initializing GHC session with following options") + (intercalate " " $ map (("\""++) . (++"\"")) opts') + + initSession opts' $ + setModeSimple >>> setEmptyLogger >>> mdf + + let rfns = map (makeRelative $ cradleRootDir crdl) cfns + + unGmlT $ wrapper $ do + loadTargets (map moduleNameString mns ++ rfns) + action + +targetGhcOptions :: forall m. IOish m + => Cradle + -> Set (Either FilePath ModuleName) + -> GhcModT m [GHCOption] +targetGhcOptions crdl sefnmn = do + when (Set.null sefnmn) $ error "targetGhcOptions: no targets given" + + case cradleProjectType crdl of + CabalProject -> cabalOpts crdl + _ -> sandboxOpts crdl + where + zipMap f l = l `zip` (f `map` l) + + cabalOpts :: Cradle -> GhcModT m [String] + cabalOpts Cradle{..} = do + mcs <- cabalResolvedComponents + + let mdlcs = moduleComponents mcs `zipMap` Set.toList sefnmn + candidates = findCandidates $ map snd mdlcs + + let noCandidates = Set.null candidates + noModuleHasAnyAssignment = all (Set.null . snd) mdlcs + + if noCandidates && noModuleHasAnyAssignment + then do + -- First component should be ChLibName, if no lib will take lexically first exe. + let cns = filter (/= ChSetupHsName) $ Map.keys mcs + + gmLog GmWarning "" $ strDoc $ "Could not find a component assignment, falling back to picking library component in cabal file." + return $ gmcGhcOpts $ fromJust $ Map.lookup (head cns) mcs + else do + when noCandidates $ + throwError $ GMECabalCompAssignment mdlcs + + let cn = pickComponent candidates + return $ gmcGhcOpts $ fromJust $ Map.lookup cn mcs + +resolvedComponentsCache :: IOish m => Cached (GhcModT m) GhcModState + [GmComponent 'GMCRaw (Set.Set ModulePath)] + (Map.Map ChComponentName (GmComponent 'GMCResolved (Set.Set ModulePath))) +resolvedComponentsCache = Cached { + cacheLens = Just (lGmcResolvedComponents . lGmCaches), + cacheFile = resolvedComponentsCacheFile, + cachedAction = \tcfs comps ma -> do + Cradle {..} <- cradle + let iifsM = invalidatingInputFiles tcfs + mums :: Maybe [Either FilePath ModuleName] + mums = + case iifsM of + Nothing -> Nothing + Just iifs -> + let + filterOutSetupCfg = + filter (/= cradleRootDir setupConfigPath) + changedFiles = filterOutSetupCfg iifs + in if null changedFiles + then Nothing + else Just $ map Left changedFiles + setupChanged = maybe False + (elem $ cradleRootDir setupConfigPath) + iifsM + case (setupChanged, ma) of + (False, Just mcs) -> gmsGet >>= \s -> gmsPut s { gmComponents = mcs } + _ -> return () + + let mdesc (Left f) = "file:" ++ f + mdesc (Right mn) = "module:" ++ moduleNameString mn + + changed = map (text . mdesc) $ Foldable.concat mums + changedDoc | [] <- changed = text "none" + | otherwise = sep changed + + gmLog GmDebug "resolvedComponentsCache" $ + text "files changed" <+>: changedDoc + + mcs <- resolveGmComponents mums comps + return (setupConfigPath:flatten mcs , mcs) + } + + where + flatten :: Map.Map ChComponentName (GmComponent t (Set.Set ModulePath)) + -> [FilePath] + flatten = Map.elems + >>> map (gmcHomeModuleGraph >>> gmgGraph + >>> Map.elems + >>> map (Set.map mpPath) + >>> Set.unions + ) + >>> Set.unions + >>> Set.toList + +moduleComponents :: Map ChComponentName (GmComponent t (Set ModulePath)) + -> Either FilePath ModuleName + -> Set ChComponentName +moduleComponents m efnmn = + foldr' Set.empty m $ \c s -> + let + memb = + case efnmn of + Left fn -> fn `Set.member` Set.map mpPath (smp c) + Right mn -> mn `Set.member` Set.map mpModule (smp c) + in if memb + then Set.insert (gmcName c) s + else s + where + smp c = Map.keysSet $ gmgGraph $ gmcHomeModuleGraph c + + foldr' b as f = Map.foldr f b as + + +findCandidates :: [Set ChComponentName] -> Set ChComponentName +findCandidates [] = Set.empty +findCandidates scns = foldl1 Set.intersection scns + +pickComponent :: Set ChComponentName -> ChComponentName +pickComponent scn = Set.findMin scn + +packageGhcOptions :: (Applicative m, IOish m, GmEnv m, GmState m, GmLog m) + => m [GHCOption] +packageGhcOptions = do + crdl <- cradle + case cradleProjectType crdl of + CabalProject -> getGhcMergedPkgOptions + _ -> sandboxOpts crdl + +-- also works for plain projects! +sandboxOpts :: MonadIO m => Cradle -> m [String] +sandboxOpts crdl = do + pkgDbStack <- liftIO $ getSandboxPackageDbStack $ cradleRootDir crdl + let pkgOpts = ghcDbStackOpts pkgDbStack + return $ ["-i" ++ d | d <- [wdir,rdir]] ++ pkgOpts ++ ["-Wall"] + where + (wdir, rdir) = (cradleCurrentDir crdl, cradleRootDir crdl) + + getSandboxPackageDbStack :: FilePath + -- ^ Project Directory (where the cabal.sandbox.config + -- file would be if it exists) + -> IO [GhcPkgDb] + getSandboxPackageDbStack cdir = + ([GlobalDb] ++) . maybe [UserDb] return <$> getSandboxDb cdir + +resolveGmComponent :: (IOish m, GmLog m, GmEnv m) + => Maybe [CompilationUnit] -- ^ Updated modules + -> GmComponent 'GMCRaw (Set ModulePath) + -> m (GmComponent 'GMCResolved (Set ModulePath)) +resolveGmComponent mums c@GmComponent {..} = do + withLightHscEnv ghcOpts $ \env -> do + let srcDirs = if null gmcSourceDirs then [""] else gmcSourceDirs + let mg = gmcHomeModuleGraph + let simp = gmcEntrypoints + sump <- case mums of + Nothing -> return simp + Just ums -> + Set.fromList . catMaybes <$> + mapM (resolveModule env srcDirs) ums + + mg' <- canonicalizeModuleGraph =<< updateHomeModuleGraph env mg simp sump + + return $ c { gmcEntrypoints = simp, gmcHomeModuleGraph = mg' } + + where ghcOpts = concat [ + gmcGhcSrcOpts, + gmcGhcLangOpts, + [ "-optP-include", "-optP" ++ macrosHeaderPath ] + ] + +resolveEntrypoint :: (IOish m, GmEnv m, GmLog m) + => Cradle + -> GmComponent 'GMCRaw ChEntrypoint + -> m (GmComponent 'GMCRaw (Set ModulePath)) +resolveEntrypoint Cradle {..} c@GmComponent {..} = do + withLightHscEnv gmcGhcSrcOpts $ \env -> do + let srcDirs = if null gmcSourceDirs then [""] else gmcSourceDirs + eps <- liftIO $ resolveChEntrypoints cradleRootDir gmcEntrypoints + rms <- resolveModule env srcDirs `mapM` eps + return c { gmcEntrypoints = Set.fromList $ catMaybes rms } + +-- TODO: remember that he file from `main-is:` is always module `Main` and let +-- ghc do the warning about it. Right now we run that module through +-- resolveModule like any other +resolveChEntrypoints :: FilePath -> ChEntrypoint -> IO [CompilationUnit] +resolveChEntrypoints _ (ChLibEntrypoint em om) = + return $ map (Right . chModToMod) (em ++ om) + +resolveChEntrypoints _ (ChExeEntrypoint main om) = + return $ [Left main] ++ map (Right . chModToMod) om + +resolveChEntrypoints srcDir ChSetupEntrypoint = do + shs <- doesFileExist (srcDir "Setup.hs") + slhs <- doesFileExist (srcDir "Setup.lhs") + return $ case (shs, slhs) of + (True, _) -> [Left "Setup.hs"] + (_, True) -> [Left "Setup.lhs"] + (False, False) -> [] + +chModToMod :: ChModuleName -> ModuleName +chModToMod (ChModuleName mn) = mkModuleName mn + +resolveModule :: (MonadIO m, GmEnv m, GmLog m) => + HscEnv -> [FilePath] -> CompilationUnit -> m (Maybe ModulePath) +resolveModule env _srcDirs (Right mn) = + liftIO $ traverse canonicalizeModulePath =<< findModulePath env mn +resolveModule env srcDirs (Left fn') = do + mfn <- liftIO $ findFile' srcDirs fn' + case mfn of + Nothing -> return Nothing + Just fn'' -> do + fn <- liftIO $ canonicalizePath fn'' + emn <- liftIO $ fileModuleName env fn + case emn of + Left errs -> do + gmLog GmWarning ("resolveModule " ++ show fn) $ + Monoid.mempty $+$ (vcat $ map text errs) + return Nothing -- TODO: should expose these errors otherwise + -- modules with preprocessor/parse errors are + -- going to be missing + Right mmn -> return $ Just $ + case mmn of + Nothing -> mkMainModulePath fn + Just mn -> ModulePath mn fn + where + -- needed for ghc 7.4 + findFile' dirs file = + getFirst . mconcat <$> mapM (fmap First . mightExist . (file)) dirs + + -- fileModuleName fn (dir:dirs) + -- | makeRelative dir fn /= fn + +type CompilationUnit = Either FilePath ModuleName + +resolveGmComponents :: (IOish m, GmState m, GmLog m, GmEnv m) + => Maybe [CompilationUnit] + -- ^ Updated modules + -> [GmComponent 'GMCRaw (Set ModulePath)] + -> m (Map ChComponentName (GmComponent 'GMCResolved (Set ModulePath))) +resolveGmComponents mumns cs = do + s <- gmsGet + m' <- foldrM' (gmComponents s) cs $ \c m -> do + case Map.lookup (gmcName c) m of + Nothing -> insertUpdated m c + Just c' -> if same gmcRawEntrypoints c c' && same gmcGhcSrcOpts c c' + then return m + else insertUpdated m c + gmsPut s { gmComponents = m' } + return m' + + where + foldrM' b fa f = foldrM f b fa + insertUpdated m c = do + rc <- resolveGmComponent mumns c + return $ Map.insert (gmcName rc) rc m + + same :: Eq b + => (forall t a. GmComponent t a -> b) + -> GmComponent u c -> GmComponent v d -> Bool + same f a b = (f a) == (f b) -- | Set the files as targets and load them. -setTargetFiles :: IOish m => [FilePath] -> GhcModT m () -setTargetFiles files = do - targets <- forM files $ \file -> G.guessTarget file Nothing - G.setTargets targets +loadTargets :: IOish m => [String] -> GmlT m () +loadTargets filesOrModules = do + gmLog GmDebug "loadTargets" $ + text "Loading" <+>: fsep (map text filesOrModules) + + targets <- forM filesOrModules (flip guessTarget Nothing) + setTargets targets + mode <- getCompilerMode - if mode == Intelligent then - loadTargets Intelligent + if mode == Intelligent + then loadTargets' Intelligent else do - mdls <- G.depanal [] False + mdls <- depanal [] False let fallback = needsFallback mdls if fallback then do resetTargets targets setIntelligent - loadTargets Intelligent + gmLog GmInfo "loadTargets" $ + text "Target needs interpeter, switching to LinkInMemory/HscInterpreted. Perfectly normal if anything is using TemplateHaskell, QuasiQuotes or PatternSynonyms." + loadTargets' Intelligent else - loadTargets Simple + loadTargets' Simple where - loadTargets Simple = do - -- Reporting error A and error B - void $ G.load LoadAllTargets - mss <- filter (\x -> G.ms_hspp_file x `elem` files) <$> G.getModuleGraph - -- Reporting error B and error C - mapM_ (G.parseModule >=> G.typecheckModule >=> G.desugarModule) mss - -- Error B duplicates. But we cannot ignore both error reportings, - -- sigh. So, the logger makes log messages unique by itself. - loadTargets Intelligent = do - df <- G.getSessionDynFlags - void $ G.setSessionDynFlags (setModeIntelligent df) - void $ G.load LoadAllTargets + loadTargets' Simple = do + void $ load LoadAllTargets + mapM_ (parseModule >=> typecheckModule >=> desugarModule) =<< getModuleGraph + + loadTargets' Intelligent = do + df <- getSessionDynFlags + void $ setSessionDynFlags (setModeIntelligent df) + void $ load LoadAllTargets + resetTargets targets = do - G.setTargets [] - void $ G.load LoadAllTargets - G.setTargets targets + setTargets [] + void $ load LoadAllTargets + setTargets targets + setIntelligent = do - newdf <- setModeIntelligent <$> G.getSessionDynFlags - void $ G.setSessionDynFlags newdf + newdf <- setModeIntelligent <$> getSessionDynFlags + void $ setSessionDynFlags newdf setCompilerMode Intelligent -needsFallback :: G.ModuleGraph -> Bool +needsFallback :: ModuleGraph -> Bool needsFallback = any $ \ms -> - let df = G.ms_hspp_opts ms in + let df = ms_hspp_opts ms in Opt_TemplateHaskell `xopt` df || Opt_QuasiQuotes `xopt` df #if __GLASGOW_HASKELL__ >= 708 || (Opt_PatternSynonyms `xopt` df) #endif + +cabalResolvedComponents :: (IOish m) => + GhcModT m (Map ChComponentName (GmComponent 'GMCResolved (Set ModulePath))) +cabalResolvedComponents = do + crdl@(Cradle{..}) <- cradle + comps <- mapM (resolveEntrypoint crdl) =<< getComponents + cached cradleRootDir resolvedComponentsCache comps diff --git a/Language/Haskell/GhcMod/Types.hs b/Language/Haskell/GhcMod/Types.hs index 46b7a35..859086c 100644 --- a/Language/Haskell/GhcMod/Types.hs +++ b/Language/Haskell/GhcMod/Types.hs @@ -1,12 +1,45 @@ -module Language.Haskell.GhcMod.Types where +{-# LANGUAGE CPP, DeriveDataTypeable, DeriveFunctor, DeriveGeneric, + StandaloneDeriving, DefaultSignatures, FlexibleInstances, TemplateHaskell #-} +{-# OPTIONS_GHC -fno-warn-orphans -fno-warn-deprecations #-} +module Language.Haskell.GhcMod.Types ( + module Language.Haskell.GhcMod.Types + , ModuleName + , mkModuleName + , moduleNameString + ) where import Control.Monad.Trans.Control (MonadBaseControl) +import Control.Monad.Error (Error(..)) +import qualified Control.Monad.IO.Class as MTL +import Control.Exception (Exception) +import Control.Applicative +import Control.Concurrent +import Control.Monad +import Data.Serialize +import Data.Version import Data.List (intercalate) -import qualified Data.Map as M +import Data.Map (Map) +import qualified Data.Map as Map +import Data.Set (Set) +import qualified Data.Set as Set +import Data.Monoid +import Data.Maybe +import Data.Typeable (Typeable) +import Data.IORef +import Data.Label.Derive +import Distribution.Helper import Exception (ExceptionMonad) -import MonadUtils (MonadIO) - +#if __GLASGOW_HASKELL__ < 708 +import qualified MonadUtils as GHC (MonadIO(..)) +#endif +import GHC (ModuleName, moduleNameString, mkModuleName) +import HscTypes (HscEnv) import PackageConfig (PackageConfig) +import GHC.Generics +import Text.PrettyPrint (Doc) +import Prelude + +import Language.Haskell.GhcMod.Caching.Types -- | A constraint alias (-XConstraintKinds) to make functions dealing with -- 'GhcModT' somewhat cleaner. @@ -16,6 +49,18 @@ import PackageConfig (PackageConfig) -- the exported API so users have the option to use a custom inner monad. type IOish m = (Functor m, MonadIO m, MonadBaseControl IO m, ExceptionMonad m) + +-- MonadUtils of GHC 7.6 or earlier defines its own MonadIO. +-- MonadUtils of GHC 7.8 or later imports MonadIO in Monad.Control.IO.Class. +#if __GLASGOW_HASKELL__ < 708 +type MonadIOC m = (GHC.MonadIO m, MTL.MonadIO m) +#else +type MonadIOC m = (MTL.MonadIO m) +#endif + +class MonadIOC m => MonadIO m where + liftIO :: IO a -> m a + -- | Output style. data OutputStyle = LispStyle -- ^ S expression style. | PlainStyle -- ^ Plain textstyle. @@ -28,8 +73,15 @@ data Options = Options { outputStyle :: OutputStyle -- | Line separator string. , lineSeparator :: LineSeparator + -- | Stdout/err line multiplexing using prefix encoding. @fst@ is stdout, + -- @snd@ is stderr prefix. + , linePrefix :: Maybe (String, String) + -- | Verbosity + , logLevel :: GmLogLevel -- | @ghc@ program name. , ghcProgram :: FilePath + -- | @ghc-pkg@ program name. + , ghcPkgProgram :: FilePath -- | @cabal@ program name. , cabalProgram :: FilePath -- | GHC command line options set on the @ghc-mod@ command line @@ -43,44 +95,114 @@ data Options = Options { , hlintOpts :: [String] } deriving (Show) - -- | A default 'Options'. defaultOptions :: Options defaultOptions = Options { - outputStyle = PlainStyle - , hlintOpts = [] - , ghcProgram = "ghc" - , cabalProgram = "cabal" - , ghcUserOptions= [] - , operators = False - , detailed = False - , qualified = False - , lineSeparator = LineSeparator "\0" + outputStyle = PlainStyle + , lineSeparator = LineSeparator "\0" + , linePrefix = Nothing + , logLevel = GmWarning + , ghcProgram = "ghc" + , ghcPkgProgram = "ghc-pkg" + , cabalProgram = "cabal" + , ghcUserOptions = [] + , operators = False + , detailed = False + , qualified = False + , hlintOpts = [] } ---------------------------------------------------------------- +data ProjectType = CabalProject | SandboxProject | PlainProject + deriving (Eq, Show) + -- | The environment where this library is used. data Cradle = Cradle { + cradleProjectType:: ProjectType -- | The directory where this library is executed. - cradleCurrentDir :: FilePath + , cradleCurrentDir :: FilePath -- | The project root directory. , cradleRootDir :: FilePath -- | Per-Project temporary directory , cradleTempDir :: FilePath -- | The file name of the found cabal file. , cradleCabalFile :: Maybe FilePath - -- | Package database stack - , cradlePkgDbStack :: [GhcPkgDb] } deriving (Eq, Show) + +data GmStream = GmOut | GmErr + deriving (Show) + +data GmLineType = GmTerminated | GmPartial + deriving (Show) + +data GmLines a = GmLines GmLineType a + deriving (Show, Functor) + +unGmLine :: GmLines a -> a +unGmLine (GmLines _ s) = s + +data GmOutput = GmOutputStdio + | GmOutputChan (Chan (GmStream, GmLines String)) + +data GhcModEnv = GhcModEnv { + gmOptions :: Options + , gmCradle :: Cradle + , gmOutput :: GmOutput + } + +data GhcModLog = GhcModLog { + gmLogLevel :: Maybe GmLogLevel, + gmLogVomitDump :: Last Bool, + gmLogMessages :: [(GmLogLevel, String, Doc)] + } deriving (Show) + +instance Monoid GhcModLog where + mempty = GhcModLog (Just GmPanic) (Last Nothing) mempty + GhcModLog ml vd ls `mappend` GhcModLog ml' vd' ls' = + GhcModLog (ml' `mplus` ml) (vd `mappend` vd') (ls `mappend` ls') + +data GmGhcSession = GmGhcSession { + gmgsOptions :: ![GHCOption], + gmgsSession :: !(IORef HscEnv) + } + +data GhcModCaches = GhcModCaches { + gmcPackageDbStack :: CacheContents ChCacheData [GhcPkgDb] + , gmcMergedPkgOptions :: CacheContents ChCacheData [GHCOption] + , gmcComponents :: CacheContents ChCacheData [GmComponent 'GMCRaw ChEntrypoint] + , gmcResolvedComponents :: CacheContents + [GmComponent 'GMCRaw (Set.Set ModulePath)] + (Map.Map ChComponentName (GmComponent 'GMCResolved (Set.Set ModulePath))) + } + +data GhcModState = GhcModState { + gmGhcSession :: !(Maybe GmGhcSession) + , gmComponents :: !(Map ChComponentName (GmComponent 'GMCResolved (Set ModulePath))) + , gmCompilerMode :: !CompilerMode + , gmCaches :: !GhcModCaches + } + +data CompilerMode = Simple | Intelligent deriving (Eq,Show,Read) + +defaultGhcModState :: GhcModState +defaultGhcModState = + GhcModState n Map.empty Simple (GhcModCaches n n n n) + where n = Nothing + ---------------------------------------------------------------- -- | GHC package database flags. -data GhcPkgDb = GlobalDb | UserDb | PackageDb String deriving (Eq, Show) +data GhcPkgDb = GlobalDb + | UserDb + | PackageDb String + deriving (Eq, Show, Generic) + +instance Serialize GhcPkgDb -- | A single GHC command line option. -type GHCOption = String +type GHCOption = String -- | An include directory for modules. type IncludeDir = FilePath @@ -89,44 +211,175 @@ type IncludeDir = FilePath type PackageBaseName = String -- | A package version. -type PackageVersion = String +type PackageVersion = String -- | A package id. -type PackageId = String +type PackageId = String -- | A package's name, verson and id. -type Package = (PackageBaseName, PackageVersion, PackageId) +type Package = (PackageBaseName, PackageVersion, PackageId) pkgName :: Package -> PackageBaseName -pkgName (n,_,_) = n +pkgName (n, _, _) = n pkgVer :: Package -> PackageVersion -pkgVer (_,v,_) = v +pkgVer (_, v, _) = v pkgId :: Package -> PackageId -pkgId (_,_,i) = i +pkgId (_, _, i) = i showPkg :: Package -> String -showPkg (n,v,_) = intercalate "-" [n,v] +showPkg (n, v, _) = intercalate "-" [n, v] showPkgId :: Package -> String -showPkgId (n,v,i) = intercalate "-" [n,v,i] - --- | Collection of packages -type PkgDb = (M.Map Package PackageConfig) +showPkgId (n, v, i) = intercalate "-" [n, v, i] -- | Haskell expression. -type Expression = String +newtype Expression = Expression { getExpression :: String } + deriving (Show, Eq, Ord) -- | Module name. -type ModuleString = String +newtype ModuleString = ModuleString { getModuleString :: String } + deriving (Show, Read, Eq, Ord) --- | A Module -type Module = [String] +data GmLogLevel = + GmSilent + | GmPanic + | GmException + | GmError + | GmWarning + | GmInfo + | GmDebug + | GmVomit + deriving (Eq, Ord, Enum, Bounded, Show, Read) --- | Option information for GHC -data CompilerOptions = CompilerOptions { - ghcOptions :: [GHCOption] -- ^ Command line options - , includeDirs :: [IncludeDir] -- ^ Include directories for modules - , depPackages :: [Package] -- ^ Dependent package names - } deriving (Eq, Show) +-- | Collection of packages +type PkgDb = (Map Package PackageConfig) + +data GmModuleGraph = GmModuleGraph { + gmgGraph :: Map ModulePath (Set ModulePath) + } deriving (Eq, Ord, Show, Read, Generic, Typeable) + +instance Serialize GmModuleGraph where + put GmModuleGraph {..} = put (mpim, graph) + where + mpim :: Map ModulePath Integer + mpim = Map.fromList $ Map.keys gmgGraph `zip` [0..] + graph :: Map Integer (Set Integer) + graph = Map.map (Set.map mpToInt) $ Map.mapKeys mpToInt gmgGraph + mpToInt :: ModulePath -> Integer + mpToInt mp = fromJust $ Map.lookup mp mpim + + get = do + (mpim :: Map ModulePath Integer, graph :: Map Integer (Set Integer)) <- get + let impm = swapMap mpim + intToMp i = fromJust $ Map.lookup i impm + mpGraph :: Map ModulePath (Set ModulePath) + mpGraph = Map.map (Set.map intToMp) $ Map.mapKeys intToMp graph + return $ GmModuleGraph mpGraph + where + swapMap :: (Ord k, Ord v) => Map k v -> Map v k + swapMap = Map.fromList . map (\(x, y) -> (y, x)) . Map.toList + +instance Monoid GmModuleGraph where + mempty = GmModuleGraph mempty + mappend (GmModuleGraph a) (GmModuleGraph a') = + GmModuleGraph (Map.unionWith Set.union a a') + +data GmComponentType = GMCRaw + | GMCResolved +data GmComponent (t :: GmComponentType) eps = GmComponent { + gmcHomeModuleGraph :: GmModuleGraph + , gmcName :: ChComponentName + , gmcGhcOpts :: [GHCOption] + , gmcGhcPkgOpts :: [GHCOption] + , gmcGhcSrcOpts :: [GHCOption] + , gmcGhcLangOpts :: [GHCOption] + , gmcRawEntrypoints :: ChEntrypoint + , gmcEntrypoints :: eps + , gmcSourceDirs :: [FilePath] + } deriving (Eq, Ord, Show, Read, Generic, Functor) + +instance Serialize eps => Serialize (GmComponent t eps) + +data ModulePath = ModulePath { mpModule :: ModuleName, mpPath :: FilePath } + deriving (Eq, Ord, Show, Read, Generic, Typeable) +instance Serialize ModulePath + +instance Serialize ModuleName where + get = mkModuleName <$> get + put mn = put (moduleNameString mn) + +instance Show ModuleName where + show mn = "ModuleName " ++ show (moduleNameString mn) + +instance Read ModuleName where + readsPrec d = + readParen + (d > app_prec) + (\r' -> [ (mkModuleName m, t) + | ("ModuleName", s) <- lex r' + , (m, t) <- readsPrec (app_prec + 1) s + ]) + where + app_prec = 10 + +data GhcModError + = GMENoMsg + -- ^ Unknown error + + | GMEString String + -- ^ Some Error with a message. These are produced mostly by + -- 'fail' calls on GhcModT. + + | GMECabalConfigure GhcModError + -- ^ Configuring a cabal project failed. + + | GMECabalFlags GhcModError + -- ^ Retrieval of the cabal configuration flags failed. + + | GMECabalComponent ChComponentName + -- ^ Cabal component could not be found + + | GMECabalCompAssignment [(Either FilePath ModuleName, Set ChComponentName)] + -- ^ Could not find a consistent component assignment for modules + + | GMEProcess String [String] (Either (String, String, Int) GhcModError) + -- ^ Launching an operating system process failed. Fields in + -- order: command, arguments, (stdout, stderr, exitcode) + + | GMENoCabalFile + -- ^ No cabal file found. + + | GMETooManyCabalFiles [FilePath] + -- ^ Too many cabal files found. + + | GMECabalStateFile GMConfigStateFileError + -- ^ Reading Cabal's state configuration file falied somehow. + deriving (Eq,Show,Typeable) + +instance Error GhcModError where + noMsg = GMENoMsg + strMsg = GMEString + +instance Exception GhcModError + +data GMConfigStateFileError + = GMConfigStateFileNoHeader + | GMConfigStateFileBadHeader + | GMConfigStateFileNoParse + | GMConfigStateFileMissing +-- | GMConfigStateFileBadVersion PackageIdentifier PackageIdentifier (Either ConfigStateFileError LocalBuildInfo) + deriving (Eq, Show, Read, Typeable) + + +deriving instance Generic Version +instance Serialize Version + +instance Serialize Programs +instance Serialize ChModuleName +instance Serialize ChComponentName +instance Serialize ChEntrypoint + +mkLabel ''GhcModCaches +mkLabel ''GhcModState diff --git a/Language/Haskell/GhcMod/Utils.hs b/Language/Haskell/GhcMod/Utils.hs index cc8420a..c9da5a2 100644 --- a/Language/Haskell/GhcMod/Utils.hs +++ b/Language/Haskell/GhcMod/Utils.hs @@ -1,94 +1,159 @@ -{-# LANGUAGE CPP #-} -module Language.Haskell.GhcMod.Utils where +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . -import Control.Arrow +{-# LANGUAGE CPP #-} +{-# LANGUAGE DoAndIfThenElse #-} + +module Language.Haskell.GhcMod.Utils ( + module Language.Haskell.GhcMod.Utils + , module Utils + , readProcess + ) where + +import Control.Applicative import Data.Char +import Exception import Language.Haskell.GhcMod.Error -import MonadUtils (MonadIO, liftIO) -import System.Directory (getCurrentDirectory, setCurrentDirectory, doesFileExist) -import System.Exit (ExitCode(..)) -import System.Process (readProcessWithExitCode) -import System.Directory (getTemporaryDirectory) -import System.FilePath (splitDrive, pathSeparators) -import System.IO.Temp (createTempDirectory) -#ifndef SPEC -import Control.Applicative ((<$>)) +import Language.Haskell.GhcMod.Monad.Types +import System.Directory (getCurrentDirectory, setCurrentDirectory, doesFileExist, + getTemporaryDirectory, canonicalizePath) import System.Environment -import System.FilePath ((),takeDirectory) -#endif +import System.FilePath (splitDrive, takeDirectory, takeFileName, pathSeparators, + ()) +import System.IO.Temp (createTempDirectory) +import System.Process (readProcess) +import Text.Printf + +import Paths_ghc_mod (getLibexecDir) +import Utils +import Prelude -- dropWhileEnd is not provided prior to base 4.5.0.0. dropWhileEnd :: (a -> Bool) -> [a] -> [a] dropWhileEnd p = foldr (\x xs -> if p x && null xs then [] else x : xs) [] -extractParens :: String -> String -extractParens str = extractParens' str 0 - where - extractParens' :: String -> Int -> String - extractParens' [] _ = [] - extractParens' (s:ss) level - | s `elem` "([{" = s : extractParens' ss (level+1) - | level == 0 = extractParens' ss 0 - | s `elem` "}])" && level == 1 = [s] - | s `elem` "}])" = s : extractParens' ss (level-1) - | otherwise = s : extractParens' ss level - -readProcess' :: (MonadIO m, MonadError GhcModError m) - => String - -> [String] - -> m String -readProcess' cmd opts = do - (rv,output,err) <- liftIO (readProcessWithExitCode cmd opts "") - `modifyError'` GMEProcess ([cmd] ++ opts) - case rv of - ExitFailure val -> do - throwError $ GMEProcess ([cmd] ++ opts) $ strMsg $ - cmd ++ " " ++ unwords opts ++ " (exit " ++ show val ++ ")" - ++ "\n" ++ err - ExitSuccess -> - return output - withDirectory_ :: (MonadIO m, ExceptionMonad m) => FilePath -> m a -> m a withDirectory_ dir action = - gbracket (liftIO getCurrentDirectory) (liftIO . setCurrentDirectory) - (\_ -> liftIO (setCurrentDirectory dir) >> action) + gbracket + (liftIO getCurrentDirectory) + (liftIO . setCurrentDirectory) + (\_ -> liftIO (setCurrentDirectory dir) >> action) uniqTempDirName :: FilePath -> FilePath -uniqTempDirName dir = ("ghc-mod"++) $ uncurry (++) - $ map escapeDriveChar *** map escapePathChar - $ splitDrive dir - where +uniqTempDirName dir = + "ghc-mod" ++ map escapeDriveChar drive ++ map escapePathChar path + where + (drive, path) = splitDrive dir + escapeDriveChar :: Char -> Char escapeDriveChar c - | isAlphaNum c = c - | otherwise = '-' - + | isAlphaNum c = c + | otherwise = '-' + escapePathChar :: Char -> Char escapePathChar c - | c `elem` pathSeparators = '-' - | otherwise = c + | c `elem` pathSeparators = '-' + | otherwise = c newTempDir :: FilePath -> IO FilePath newTempDir dir = - flip createTempDirectory (uniqTempDirName dir) =<< getTemporaryDirectory + flip createTempDirectory (uniqTempDirName dir) =<< getTemporaryDirectory -mightExist :: FilePath -> IO (Maybe FilePath) -mightExist f = do - exists <- doesFileExist f - return $ if exists then (Just f) else (Nothing) +whenM :: Monad m => m Bool -> m () -> m () +whenM mb ma = mb >>= flip when ma -- | Returns the path to the currently running ghc-mod executable. With ghc<7.6 -- this is a guess but >=7.6 uses 'getExecutablePath'. ghcModExecutable :: IO FilePath #ifndef SPEC ghcModExecutable = do - dir <- getExecutablePath' - return $ dir "ghc-mod" - where - getExecutablePath' :: IO FilePath -# if __GLASGOW_HASKELL__ >= 706 - getExecutablePath' = takeDirectory <$> getExecutablePath -# else - getExecutablePath' = return "" -# endif + dir <- takeDirectory <$> getExecutablePath' + return $ (if dir == "." then "" else dir) "ghc-mod" #else -ghcModExecutable = return "dist/build/ghc-mod/ghc-mod" +ghcModExecutable = fmap ( "dist/build/ghc-mod/ghc-mod") getCurrentDirectory #endif + +findLibexecExe :: String -> IO FilePath +findLibexecExe "cabal-helper-wrapper" = do + libexecdir <- getLibexecDir + let exeName = "cabal-helper-wrapper" + exe = libexecdir exeName + + exists <- doesFileExist exe + + if exists + then return exe + else do + mdir <- tryFindGhcModTreeDataDir + case mdir of + Nothing -> + error $ libexecNotExitsError exeName libexecdir + Just dir -> + return $ dir "dist" "build" exeName exeName +findLibexecExe exe = error $ "findLibexecExe: Unknown executable: " ++ exe + +libexecNotExitsError :: String -> FilePath -> String +libexecNotExitsError exe dir = printf + ( "Could not find $libexecdir/%s\n" + ++"\n" + ++"If you are a developer set the environment variable `ghc_mod_libexecdir'\n" + ++"to override $libexecdir[1] the following will work in the ghc-mod tree:\n" + ++"\n" + ++" $ export ghc_mod_libexecdir=$PWD/dist/build/%s\n" + ++"\n" + ++"[1]: %s\n" + ++"\n" + ++"If you don't know what I'm talking about something went wrong with your\n" + ++"installation. Please report this problem here:\n" + ++"\n" + ++" https://github.com/kazu-yamamoto/ghc-mod/issues") exe exe dir + +tryFindGhcModTreeLibexecDir :: IO (Maybe FilePath) +tryFindGhcModTreeLibexecDir = do + exe <- getExecutablePath' + dir <- case takeFileName exe of + "ghc" -> getCurrentDirectory -- we're probably in ghci; try CWD + _ -> return $ (!!4) $ iterate takeDirectory exe + exists <- doesFileExist $ dir "ghc-mod.cabal" + return $ if exists + then Just dir + else Nothing + +tryFindGhcModTreeDataDir :: IO (Maybe FilePath) +tryFindGhcModTreeDataDir = do + dir <- (!!4) . iterate takeDirectory <$> getExecutablePath' + exists <- doesFileExist $ dir "ghc-mod.cabal" + return $ if exists + then Just dir + else Nothing + +readLibExecProcess' :: (MonadIO m, ExceptionMonad m) + => String -> [String] -> m String +readLibExecProcess' cmd args = do + exe <- liftIO $ findLibexecExe cmd + liftIO $ readProcess exe args "" + +getExecutablePath' :: IO FilePath +#if __GLASGOW_HASKELL__ >= 706 +getExecutablePath' = getExecutablePath +#else +getExecutablePath' = getProgName +#endif + +canonFilePath :: FilePath -> IO FilePath +canonFilePath f = do + p <- canonicalizePath f + e <- doesFileExist p + when (not e) $ error $ "canonFilePath: not a file: " ++ p + return p diff --git a/Language/Haskell/GhcMod/World.hs b/Language/Haskell/GhcMod/World.hs index 83b874f..e887990 100644 --- a/Language/Haskell/GhcMod/World.hs +++ b/Language/Haskell/GhcMod/World.hs @@ -1,89 +1,52 @@ -{-# LANGUAGE CPP #-} module Language.Haskell.GhcMod.World where -{-( - , World - , getCurrentWorld - , isWorldChanged - ) where --} import Language.Haskell.GhcMod.GhcPkg import Language.Haskell.GhcMod.PathsAndFiles import Language.Haskell.GhcMod.Types +import Language.Haskell.GhcMod.Monad.Types import Language.Haskell.GhcMod.Utils -import Control.Applicative (pure,(<$>),(<*>)) +import Control.Applicative import Data.Maybe -import Data.Traversable (traverse) -import System.Directory (getModificationTime) +import Data.Traversable hiding (mapM) import System.FilePath (()) import GHC.Paths (libdir) - -#if __GLASGOW_HASKELL__ <= 704 -import System.Time (ClockTime) -#else -import Data.Time (UTCTime) -#endif - - -#if __GLASGOW_HASKELL__ <= 704 -type ModTime = ClockTime -#else -type ModTime = UTCTime -#endif - -data TimedFile = TimedFile FilePath ModTime deriving (Eq, Show) - -instance Ord TimedFile where - compare (TimedFile _ a) (TimedFile _ b) = compare a b - -timeFile :: FilePath -> IO TimedFile -timeFile f = TimedFile <$> pure f <*> getModificationTime f +import Prelude data World = World { worldPackageCaches :: [TimedFile] , worldCabalFile :: Maybe TimedFile , worldCabalConfig :: Maybe TimedFile + , worldSymbolCache :: Maybe TimedFile } deriving (Eq, Show) -timedPackageCache :: Cradle -> IO [TimedFile] -timedPackageCache crdl = do - fs <- mapM mightExist . map ( packageCache) - =<< getPackageCachePaths libdir crdl - timeFile `mapM` catMaybes fs +timedPackageCaches :: IOish m => GhcModT m [TimedFile] +timedPackageCaches = do + fs <- mapM (liftIO . mightExist) . map ( packageCache) + =<< getPackageCachePaths libdir + (liftIO . timeFile) `mapM` catMaybes fs -getCurrentWorld :: Cradle -> IO World -getCurrentWorld crdl = do - pkgCaches <- timedPackageCache crdl - mCabalFile <- timeFile `traverse` cradleCabalFile crdl - mSetupConfig <- mightExist (setupConfigFile crdl) - mCabalConfig <- timeFile `traverse` mSetupConfig +getCurrentWorld :: IOish m => GhcModT m World +getCurrentWorld = do + crdl <- cradle + pkgCaches <- timedPackageCaches + mCabalFile <- liftIO $ timeFile `traverse` cradleCabalFile crdl + mCabalConfig <- liftIO $ timeMaybe (setupConfigFile crdl) + mSymbolCache <- liftIO $ timeMaybe (symbolCache crdl) return World { worldPackageCaches = pkgCaches , worldCabalFile = mCabalFile , worldCabalConfig = mCabalConfig + , worldSymbolCache = mSymbolCache } -didWorldChange :: World -> Cradle -> IO Bool -didWorldChange world crdl = do - (world /=) <$> getCurrentWorld crdl +didWorldChange :: IOish m => World -> GhcModT m Bool +didWorldChange world = do + (world /=) <$> getCurrentWorld --- * Neither file exists -> should return False: --- @Nothing < Nothing = False@ --- (since we don't need to @cabal configure@ when no cabal file exists.) --- --- * Cabal file doesn't exist (unlikely case) -> should return False --- @Just cc < Nothing = False@ --- TODO: should we delete dist/setup-config? --- --- * dist/setup-config doesn't exist yet -> should return True: --- @Nothing < Just cf = True@ --- --- * Both files exist --- @Just cc < Just cf = cc < cf = cc `olderThan` cf@ -isSetupConfigOutOfDate :: Cradle -> IO Bool -isSetupConfigOutOfDate crdl = do - world <- getCurrentWorld crdl - return $ worldCabalConfig world < worldCabalFile world +isYoungerThanSetupConfig :: FilePath -> World -> IO Bool +isYoungerThanSetupConfig file World {..} = do + tfile <- timeFile file + return $ worldCabalConfig < Just tfile diff --git a/Language/Haskell/GhcMod/Cabal16.hs b/NotCPP/COPYING similarity index 71% rename from Language/Haskell/GhcMod/Cabal16.hs rename to NotCPP/COPYING index d36fc36..9eb8e81 100644 --- a/Language/Haskell/GhcMod/Cabal16.hs +++ b/NotCPP/COPYING @@ -1,9 +1,9 @@ --- Copyright : Isaac Jones 2003-2004 -{- All rights reserved. +Copyright Ben Millwood 2012 + +All rights reserved. Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: +modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. @@ -13,7 +13,7 @@ met: disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Isaac Jones nor the names of other + * Neither the name of Ben Millwood nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. @@ -27,19 +27,4 @@ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -} - - --- | ComponentLocalBuildInfo for Cabal <= 1.16 -module Language.Haskell.GhcMod.Cabal16 ( - ComponentLocalBuildInfo - , componentPackageDeps - ) where - -import Distribution.Package (InstalledPackageId, PackageIdentifier) - --- From Cabal <= 1.16 -data ComponentLocalBuildInfo = ComponentLocalBuildInfo { - componentPackageDeps :: [(InstalledPackageId, PackageIdentifier)] - } - deriving (Read, Show) +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/NotCPP/Declarations.hs b/NotCPP/Declarations.hs new file mode 100644 index 0000000..1657a68 --- /dev/null +++ b/NotCPP/Declarations.hs @@ -0,0 +1,164 @@ +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +{-# OPTIONS_GHC -fno-warn-unused-imports #-} +{-# LANGUAGE CPP #-} +-- Using CPP so you don't have to :) +module NotCPP.Declarations where + +import Control.Arrow +import Control.Applicative +import Data.Maybe +import Language.Haskell.TH.Syntax + +import NotCPP.LookupValueName + +nT :: Monad m => String -> m Type +cT :: Monad m => String -> m Type +nE :: Monad m => String -> m Exp +nP :: Monad m => String -> m Pat + +nT str = return $ VarT (mkName str) +cT str = return $ ConT (mkName str) +nE str = return $ VarE (mkName str) +nP str = return $ VarP (mkName str) +recUpdE' :: Q Exp -> Name -> Exp -> Q Exp +recUpdE' ex name assign = do + RecUpdE <$> ex <*> pure [(name, assign)] + +lookupName' :: (NameSpace, String) -> Q (Maybe Name) +lookupName' (VarName, n) = lookupValueName n +lookupName' (DataName, n) = lookupValueName n +lookupName' (TcClsName, n) = lookupTypeName n + +-- Does this even make sense? +ifelseD :: Q [Dec] -> Q [Dec] -> Q [Dec] +ifelseD if_decls' else_decls = do + if_decls <- if_decls' + alreadyDefined <- definedNames (boundNames `concatMap` if_decls) + case alreadyDefined of + [] -> if_decls' + _ -> else_decls + +ifdefelseD, ifelsedefD :: String -> Q [Dec] -> Q [Dec] -> Q [Dec] +ifelsedefD = ifdefelseD +ifdefelseD ident if_decls else_decls = do + exists <- isJust <$> lookupValueName ident + if exists + then if_decls + else else_decls + +ifdefD :: String -> Q [Dec] -> Q [Dec] +ifdefD ident decls = ifdefelseD ident decls (return []) + +ifndefD :: String -> Q [Dec] -> Q [Dec] +ifndefD ident decls = ifdefelseD ident (return []) decls + +-- | Each of the given declarations is only spliced if the identifier it defines +-- is not defined yet. +-- +-- For example: +-- +-- @$(ifD [[d| someFunctionThatShouldExist x = x+1 |]]@ +-- +-- If @someFunctionThatShouldExist@ doesn't actually exist the definition given +-- in the splice will be the result of the splice otherwise nothing will be +-- spliced. +-- +-- Currently this only works for function declarations but it can be easily +-- extended to other kinds of declarations. +ifD :: Q [Dec] -> Q [Dec] +ifD decls' = do + decls <- decls' + concat <$> flip mapM decls (\decl -> do + alreadyDefined <- definedNames (boundNames decl) + case alreadyDefined of + [] -> return [decl] + _ -> return []) + +definedNames :: [(NameSpace, Name)] -> Q [Name] +definedNames ns = catMaybes <$> (lookupName' . second nameBase) `mapM` ns + +boundNames :: Dec -> [(NameSpace, Name)] +boundNames decl = + case decl of + SigD n _ -> [(VarName, n)] + FunD n _cls -> [(VarName, n)] +#if __GLASGOW_HASKELL__ >= 706 + InfixD _ n -> [(VarName, n)] +#endif + ValD p _ _ -> map ((,) VarName) $ patNames p + + TySynD n _ _ -> [(TcClsName, n)] + ClassD _ n _ _ _ -> [(TcClsName, n)] + FamilyD _ n _ _ -> [(TcClsName, n)] + + DataD _ n _ ctors _ -> + [(TcClsName, n)] ++ map ((,) TcClsName) (conNames `concatMap` ctors) + + NewtypeD _ n _ ctor _ -> + [(TcClsName, n)] ++ map ((,) TcClsName) (conNames ctor) + + DataInstD _ _n _ ctors _ -> + map ((,) TcClsName) (conNames `concatMap` ctors) + + NewtypeInstD _ _n _ ctor _ -> + map ((,) TcClsName) (conNames ctor) + + InstanceD _ _ty _ -> + error "notcpp: Instance declarations are not supported yet" + ForeignD _ -> + error "notcpp: Foreign declarations are not supported yet" + PragmaD _pragma -> error "notcpp: pragmas are not supported yet" + +#if __GLASGOW_HASKELL__ >= 708 + TySynInstD _n _ -> error "notcpp: TySynInstD not supported yet" +#else + TySynInstD _n _ _ -> error "notcpp: TySynInstD not supported yet" +#endif + +#if __GLASGOW_HASKELL__ >= 708 + ClosedTypeFamilyD n _ _ _ -> [(TcClsName, n)] + RoleAnnotD _n _ -> error "notcpp: RoleAnnotD not supported yet" +#endif + +conNames :: Con -> [Name] +conNames con = + case con of + NormalC n _ -> [n] + RecC n _ -> [n] + InfixC _ n _ -> [n] + ForallC _ _ c -> conNames c + +patNames :: Pat -> [Name] +patNames p'' = + case p'' of + LitP _ -> [] + VarP n -> [n] + TupP ps -> patNames `concatMap` ps + UnboxedTupP ps -> patNames `concatMap` ps + ConP _ ps -> patNames `concatMap` ps + InfixP p _ p' -> patNames `concatMap` [p,p'] + UInfixP p _ p' -> patNames `concatMap` [p,p'] + ParensP p -> patNames p + TildeP p -> patNames p + BangP p -> patNames p + AsP n p -> n:(patNames p) + WildP -> [] + RecP _ fps -> patNames `concatMap` map snd fps + ListP ps -> patNames `concatMap` ps + SigP p _ -> patNames p + ViewP _ p -> patNames p diff --git a/NotCPP/LookupValueName.hs b/NotCPP/LookupValueName.hs new file mode 100644 index 0000000..72462c2 --- /dev/null +++ b/NotCPP/LookupValueName.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE TemplateHaskell #-} +-- | This module uses scope lookup techniques to either export +-- 'lookupValueName' from @Language.Haskell.TH@, or define +-- its own 'lookupValueName', which attempts to do the +-- same job with just 'reify'. This will sometimes fail, but if it +-- succeeds it will give the answer that the real function would have +-- given. +-- +-- The idea is that if you use lookupValueName from this module, +-- your client code will automatically use the best available name +-- lookup mechanism. This means that e.g. 'scopeLookup' can work +-- very well on recent GHCs and less well but still somewhat +-- usefully on older GHCs. +module NotCPP.LookupValueName ( + lookupValueName + ) where + +import Language.Haskell.TH + +import NotCPP.Utils + +bestValueGuess :: String -> Q (Maybe Name) +bestValueGuess s = do + mi <- maybeReify (mkName s) + case mi of + Nothing -> no + Just i -> case i of + VarI n _ _ _ -> yes n + DataConI n _ _ _ -> yes n + _ -> err ["unexpected info:", show i] + where + no = return Nothing + yes = return . Just + err = fail . showString "NotCPP.bestValueGuess: " . unwords + +$(recover [d| lookupValueName = bestValueGuess |] $ do + VarI _ _ _ _ <- reify (mkName "lookupValueName") + return []) diff --git a/NotCPP/OrphanEvasion.hs b/NotCPP/OrphanEvasion.hs new file mode 100644 index 0000000..d666d7b --- /dev/null +++ b/NotCPP/OrphanEvasion.hs @@ -0,0 +1,114 @@ +{-# LANGUAGE EmptyDataDecls, TemplateHaskell #-} +-- | +-- The orphan instance problem is well-known in Haskell. This module +-- by no means purports to solve the problem, but provides a workaround +-- that may be significantly less awful than the status quo in some +-- cases. +-- +-- Say I think that the 'Name' type should have an 'IsString' instance. +-- But I don't control either the class or the type, so if I define the +-- instance, and then the template-haskell package defines one, my code +-- is going to break. +-- +-- 'safeInstance' can help me to solve this problem: +-- +-- > safeInstance ''IsString [t| Name |] [d| +-- > fromString = mkName |] +-- +-- This will declare an instance only if one doesn't already exist. +-- Now anyone importing your module is guaranteed to get an instance +-- one way or the other. +-- +-- This module is still highly experimental. The example given above +-- does work, but anything involving type variables or complex method +-- bodies may be less fortunate. The names of the methods are mangled +-- a bit, so using recursion to define them may not work. Define the +-- method outside the code and then use a simple binding as above. +-- +-- If you use this code (successfully or unsuccessfully!), go fetch +-- the maintainer address from the cabal file and let me know! +module NotCPP.OrphanEvasion ( + MultiParams, + safeInstance, + safeInstance', + ) where + +import Control.Applicative + +import Language.Haskell.TH +import Language.Haskell.TH.Syntax + +import NotCPP.ScopeLookup + +-- | An empty type used only to signify a multiparameter typeclass in +-- 'safeInstance'. +data MultiParams a + +-- | Given @(forall ts. Cxt => t)@, return @(Cxt, [t])@. +-- Given @(forall ts. Cxt => 'MultiParams' (t1, t2, t3))@, return +-- @(Cxt, [t1, t2, t3])@. +-- +-- This is used in 'safeInstance' to allow types to be specified more +-- easily with TH typequotes. +fromTuple :: Type -> (Cxt, [Type]) +fromTuple ty = unTuple <$> case ty of + ForallT _ cxt ty' -> (cxt, ty') + _ -> ([], ty) + where + unTuple :: Type -> [Type] + unTuple (AppT (ConT n) ta) + | n == ''MultiParams = case unrollAppT ta of + (TupleT{}, ts) -> ts + _ -> [ty] + unTuple t = [t] + +-- | A helper function to unwind type application. +-- Given @TyCon t1 t2 t3@, returns @(TyCon, [t1,t2,t3])@ +unrollAppT :: Type -> (Type, [Type]) +unrollAppT = go [] + where + go acc (AppT tc ta) = go (ta : acc) tc + go acc ty = (ty, reverse acc) + +-- | Left inverse to unrollAppT, equal to @'foldl' 'AppT'@ +rollAppT :: Type -> [Type] -> Type +rollAppT = foldl AppT + +-- | @'safeInstance'' className cxt types methods@ produces an instance +-- of the given class if and only if one doesn't already exist. +-- +-- See 'safeInstance' for a simple way to construct the 'Cxt' and +-- @['Type']@ parameters. +safeInstance' :: Name -> Cxt -> [Type] -> Q [Dec] -> Q [Dec] +safeInstance' cl cxt tys inst = do + b <- $(scopeLookups ["isInstance", "isClassInstance"]) cl tys + if b + then return [] + else do + ds <- map fixInst <$> inst + return [InstanceD cxt (rollAppT (ConT cl) tys) ds] + where + fixInst (FunD n cls) = FunD (fixName n) cls + fixInst (ValD (VarP n) rhs wh) = ValD (VarP (fixName n)) rhs wh + fixInst d = d + fixName (Name n _) = Name n NameS + +-- | 'safeInstance' is a more convenient version of 'safeInstance'' +-- that takes the context and type from a @'Q' 'Type'@ with the intention +-- that it be supplied using a type-quote. +-- +-- To define an instance @Show a => Show (Wrapper a)@, you'd use: +-- +-- > safeInstance ''Show [t| Show a => Wrapper a |] +-- > [d| show _ = "stuff" |] +-- +-- To define an instance of a multi-param type class, use the +-- 'MultiParams' type constructor with a tuple: +-- +-- > safeInstance ''MonadState +-- > [t| MonadState s m => MultiParams (s, MaybeT m) |] +-- > [d| put = ... |] +safeInstance :: Name -> Q Type -> Q [Dec] -> Q [Dec] +safeInstance n qty inst = do + (cxt, tys) <- fromTuple <$> qty + safeInstance' n cxt tys inst diff --git a/NotCPP/ScopeLookup.hs b/NotCPP/ScopeLookup.hs new file mode 100644 index 0000000..5fb6415 --- /dev/null +++ b/NotCPP/ScopeLookup.hs @@ -0,0 +1,65 @@ +{-# LANGUAGE TemplateHaskell #-} +-- | +-- This module exports 'scopeLookup', which will find a variable or +-- value constructor for you and present it for your use. E.g. at some +-- point in the history of the acid-state package, 'openAcidState' was +-- renamed 'openLocalState'; for compatibility with both, you could +-- use: +-- +-- > openState :: IO (AcidState st) +-- > openState = case $(scopeLookup "openLocalState") of +-- > Just open -> open defaultState +-- > Nothing -> case $(scopeLookup "openAcidState") of +-- > Just open -> open defaultState +-- > Nothing -> error +-- > "openState: runtime name resolution has its drawbacks :/" +-- +-- Or, for this specific case, you can use 'scopeLookups': +-- +-- > openState :: IO (AcidState st) +-- > openState = open defaultState +-- > where +-- > open = $(scopeLookups ["openLocalState","openAcidState"]) +-- +-- Now if neither of the names are found then TH will throw a +-- compile-time error. +module NotCPP.ScopeLookup ( + scopeLookup, + scopeLookups, + scopeLookup', + liftMaybe, + recoverMaybe, + maybeReify, + infoToExp, + ) where + +import Control.Applicative ((<$>)) + +import Language.Haskell.TH (Q, Exp, recover, reify) + +import NotCPP.LookupValueName +import NotCPP.Utils + +-- | Produces a spliceable expression which expands to @'Just' val@ if +-- the given string refers to a value @val@ in scope, or 'Nothing' +-- otherwise. +-- +-- @scopeLookup = 'fmap' 'liftMaybe' . 'scopeLookup''@ +scopeLookup :: String -> Q Exp +scopeLookup = fmap liftMaybe . scopeLookup' + +-- | Finds the first string in the list that names a value, and produces +-- a spliceable expression of that value, or reports a compile error if +-- it fails. +scopeLookups :: [String] -> Q Exp +scopeLookups xs = foldr + (\s r -> maybe r return =<< scopeLookup' s) + (fail ("scopeLookups: none found: " ++ show xs)) + xs + +-- | Produces @'Just' x@ if the given string names the value @x@, +-- or 'Nothing' otherwise. +scopeLookup' :: String -> Q (Maybe Exp) +scopeLookup' s = recover (return Nothing) $ do + Just n <- lookupValueName s + infoToExp <$> reify n diff --git a/NotCPP/Utils.hs b/NotCPP/Utils.hs new file mode 100644 index 0000000..9da7958 --- /dev/null +++ b/NotCPP/Utils.hs @@ -0,0 +1,29 @@ +{-# LANGUAGE TemplateHaskell #-} +module NotCPP.Utils where + +import Control.Applicative ((<$>)) +import Language.Haskell.TH + +-- | Turns 'Nothing' into an expression representing 'Nothing', and +-- @'Just' x@ into an expression representing 'Just' applied to the +-- expression in @x@. +liftMaybe :: Maybe Exp -> Exp +liftMaybe = maybe (ConE 'Nothing) (AppE (ConE 'Just)) + +-- | A useful variant of 'reify' that returns 'Nothing' instead of +-- halting compilation when an error occurs (e.g. because the given +-- name was not in scope). +maybeReify :: Name -> Q (Maybe Info) +maybeReify = recoverMaybe . reify + +-- | Turns a possibly-failing 'Q' action into one returning a 'Maybe' +-- value. +recoverMaybe :: Q a -> Q (Maybe a) +recoverMaybe q = recover (return Nothing) (Just <$> q) + +-- | Returns @'Just' ('VarE' n)@ if the info relates to a value called +-- @n@, or 'Nothing' if it relates to a different sort of thing. +infoToExp :: Info -> Maybe Exp +infoToExp (VarI n _ _ _) = Just (VarE n) +infoToExp (DataConI n _ _ _) = Just (ConE n) +infoToExp _ = Nothing diff --git a/README.md b/README.md index 54e474d..0752888 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,27 @@ Make sure you're not using the MELPA version of `ghc.el` otherwise you might get all sorts of nasty conflicts. +## Custom ghc-mod cradle + +To customize the package databases used by `ghc-mod`, put a file called `ghc-mod.cradle` beside the `.cabal` file with the following syntax: + +``` +temp directory root +package db 1 +... +package db n +``` + +each package database line is either a *path* to a package database, or `global` or `user`. + ## IRC If you have any problems, suggestions, comments swing by -[#ghc-mod](irc://chat.freenode.net/ghc-mod) on Freenode. +[\#ghc-mod (web client)](https://kiwiirc.com/client/irc.freenode.org/ghc-mod) on +Freenode. If you're reporting a bug please also create an issue +[here](https://github.com/kazu-yamamoto/ghc-mod/issues) so we have a way to contact +you if you don't have time to stay. + +Do hang around for a while if no one answers and repeat your question if you +still haven't gotten any answer after a day or so. You're most likely to get an +answer during the day in GMT+1. diff --git a/Setup.hs b/Setup.hs old mode 100644 new mode 100755 index 9a994af..982ec70 --- a/Setup.hs +++ b/Setup.hs @@ -1,2 +1,198 @@ +#!/usr/bin/env runhaskell +{-# LANGUAGE RecordWildCards #-} import Distribution.Simple -main = defaultMain +import Distribution.Simple.Setup +import Distribution.Simple.Install +import Distribution.Simple.Register +import Distribution.Simple.InstallDirs as ID +import Distribution.Simple.LocalBuildInfo +import Distribution.PackageDescription + +import Control.Arrow +import Control.Applicative +import Control.Monad +import Data.List +import Data.Maybe +import Data.Version +import Data.Monoid +import System.Process +import System.Exit +import System.FilePath +import Text.ParserCombinators.ReadP + +import SetupCompat + +main :: IO () +main = defaultMainWithHooks $ simpleUserHooks { + confHook = \(gpd, hbi) cf -> + xBuildDependsLike <$> (confHook simpleUserHooks) (gpd, hbi) cf + + , instHook = inst + , copyHook = copy + +-- , postConf = sanityCheckCabalVersions + } + +xBuildDependsLike :: LocalBuildInfo -> LocalBuildInfo +xBuildDependsLike lbi = + let + cc = componentsConfigs lbi + pd = localPkgDescr lbi + deps = dependsMap lbi + in setComponentsConfigs lbi + [ (cn, updateClbi deps comp clbi, cdeps) + | (cn, clbi, cdeps) <- cc + , let comp = getComponent pd cn + ] + + where + updateClbi deps comp clbi = setUnionDeps (otherDeps deps comp) clbi + + dependsMap :: + LocalBuildInfo -> [(ComponentName, Deps)] + dependsMap lbi = + second getDeps <$> allComponentsInBuildOrder lbi + + otherDeps :: [(ComponentName, Deps)] -> Component -> Deps + otherDeps deps comp = fromMaybe noDeps $ + flip lookup deps =<< read <$> lookup "x-build-depends-like" fields + where + fields = customFieldsBI (componentBuildInfo comp) + +-- mostly copypasta from 'defaultInstallHook' +inst :: + PackageDescription -> LocalBuildInfo -> UserHooks -> InstallFlags -> IO () +inst pd lbi _uf ifl = do + let copyFlags = defaultCopyFlags { + copyDistPref = installDistPref ifl, + copyDest = toFlag NoCopyDest, + copyVerbosity = installVerbosity ifl + } + xInstallTarget pd lbi (\pd' lbi' -> install pd' lbi' copyFlags) + let registerFlags = defaultRegisterFlags { + regDistPref = installDistPref ifl, + regInPlace = installInPlace ifl, + regPackageDB = installPackageDB ifl, + regVerbosity = installVerbosity ifl + } + when (hasLibs pd) $ register pd lbi registerFlags + +copy :: PackageDescription -> LocalBuildInfo -> UserHooks -> CopyFlags -> IO () +copy pd lbi _uh cf = + xInstallTarget pd lbi (\pd' lbi' -> install pd' lbi' cf) + +xInstallTarget :: PackageDescription + -> LocalBuildInfo + -> (PackageDescription -> LocalBuildInfo -> IO ()) + -> IO () +xInstallTarget pd lbi fn = do + let (extended, regular) = partition (isJust . installTarget) (executables pd) + + let pd_regular = pd { executables = regular } + + _ <- flip mapM extended $ \exe -> do + putStrLn $ "extended " ++ show (exeName exe) + + let + idirtpl = installDirTemplates lbi + env = installDirsTemplateEnv idirtpl + libexecdir' = fromPathTemplate (libexecdir idirtpl) + + pd_extended = onlyExePackageDesc [exe] pd + install_target = fromJust $ installTarget exe + install_target' = ID.substPathTemplate env install_target + -- $libexec isn't a real thing :/ so we have to simulate it + install_target'' = substLibExec' libexecdir' install_target' + + let lbi' = lbi { + installDirTemplates = + (installDirTemplates lbi) { + bindir = install_target'' + } + } + fn pd_extended lbi' + + fn pd_regular lbi + + where + installTarget :: Executable -> Maybe PathTemplate + installTarget exe = + toPathTemplate <$> lookup "x-install-target" (customFieldsBI $ buildInfo exe) + + substLibExec libexecdir "$libexecdir" = libexecdir + substLibExec _ comp = comp + + substLibExec' dir = + withPT $ + withSP $ map (substLibExec dir . dropTrailingPathSeparator) + + + withPT f pt = toPathTemplate $ f (fromPathTemplate pt) + withSP f p = joinPath $ f (splitPath p) + +onlyExePackageDesc :: [Executable] -> PackageDescription -> PackageDescription +onlyExePackageDesc exes pd = emptyPackageDescription { + package = package pd + , executables = exes + } + +parseVer str = + case filter ((=="") . snd) $ readP_to_S parseVersion str of + [(ver, _)] -> ver + _ -> error $ "No parse (Ver) :(\n" ++ str ++ "\n" + +-- sanityCheckCabalVersions args cf desc lbi = do +-- (cabalInstallVer, cabalVer) <- getCabalExecVer + +-- let +-- ghcVer = compilerVersion (compiler lbi) +-- -- ghc >= 7.10? +-- minGhc710 = ghcVer `withinRange` orLaterVersion (parseVer "7.10") + +-- when minGhc710 $ do +-- let cabalHelperCabalVer = compCabalVer (CExeName "cabal-helper") + +-- when (not $ cabalVer `sameMajorVersionAs` cabalHelperCabalVer) $ +-- failCabalVersionDifferent cabalVer cabalHelperCabalVer + +-- -- carry on as usual +-- (postConf simpleUserHooks) args cf desc lbi + +-- where +-- earlierVersionThan ver ver' = +-- ver `withinRange` earlierVersion ver' +-- sameMajorVersionAs ver ver' = +-- ver `withinRange` withinVersion (Version (take 2 $ versionBranch ver') []) + +-- compCabalVer comp = let +-- clbi = getComponentLocalBuildInfo lbi comp + +-- [cabalVer] = +-- [ ver | (_, PackageIdentifier pkg ver) <- componentPackageDeps clbi +-- , pkg == PackageName "Cabal" ] +-- in cabalVer + +-- getCabalExecVer = do +-- ["cabal-install", "version", cabalInstallVer, "using", "version", cabalVer, "of", "the", "Cabal", "library"] <- words <$> readProcess "cabal" ["--version"] "" +-- return (parseVer cabalInstallVer, parseVer cabalVer) + +-- failCabalVersionDifferent cabalVer libCabalVer = +-- putStrLn rerr >> exitFailure +-- where +-- replace :: String -> String -> String -> String +-- replace _ _ [] = [] +-- replace n r h@(h':hs) +-- | map snd (n `zip` h ) == n = r ++ replace n r (drop (length n) h) +-- | otherwise = h':replace n r hs + +-- rerr = replace "X.XX.X.X" (showVersion libCabalVer) $ +-- replace "Y.YY.Y.Y" (showVersion cabalVer) err +-- err = "\ +-- \Error: Cabal seems to have decided ghc-mod should be built using Cabal\n\ +-- \X.XX.X.X while the `cabal' executable in your PATH was built with Cabal\n\ +-- \Y.YY.Y.Y. This will lead to conflicts when running ghc-mod in any project\n\ +-- \where you use this `cabal' executable. Please compile ghc-mod using the same\n\ +-- \Cabal version as your `cabal' executable or recompile cabal-install using\n\ +-- \this version of the Cabal library.\n\ +-- \\n\ +-- \See: https://github.com/kazu-yamamoto/ghc-mod/wiki/InconsistentCabalVersions\n" diff --git a/SetupCompat.hs b/SetupCompat.hs new file mode 100644 index 0000000..b35b3eb --- /dev/null +++ b/SetupCompat.hs @@ -0,0 +1,198 @@ +{-# LANGUAGE TemplateHaskell, RecordWildCards, StandaloneDeriving #-} +module SetupCompat where + +import Control.Arrow +import Control.Monad.Trans.State +import Data.List +import Data.Maybe +import Data.Functor +import Data.Function +import Distribution.Simple.LocalBuildInfo +import Distribution.PackageDescription + +import Distribution.Simple +import Distribution.Simple.Setup +import Distribution.Simple.Install + +import qualified Data.Map as M +import Data.Map (Map) + + +import NotCPP.Declarations +import Language.Haskell.TH + +-- $(ifdefD "componentsConfigs" [d| deriving instance (Ord ComponentName) |] ) + +$(ifD [d| + + showComponentName :: ComponentName -> String + showComponentName CLibName = "library" + showComponentName (CExeName name) = "executable '" ++ name ++ "'" + showComponentName (CTestName name) = "test suite '" ++ name ++ "'" + showComponentName (CBenchName name) = "benchmark '" ++ name ++ "'" + + |]) + +$(ifelsedefD "componentsConfigs" [d| + + setComponentsConfigs + :: LocalBuildInfo + -> [(ComponentName, ComponentLocalBuildInfo, [ComponentName])] + -> LocalBuildInfo + setComponentsConfigs lbi cs = $(recUpdE' (nE "lbi") (mkName "componentsConfigs") (VarE $ mkName "cs")) + + |] [d| + + setComponentsConfigs + :: LocalBuildInfo + -> [(ComponentName, ComponentLocalBuildInfo, a)] + -> LocalBuildInfo + setComponentsConfigs lbi cs = flip execState lbi $ mapM setClbis gcs + where + gcs = groupBy (sameKind `on` fst3) $ sortBy (compare `on` showComponentName . fst3) cs + + fst3 (x,_,_) = x + + sameKind CLibName CLibName = True + sameKind CLibName _ = False + sameKind (CExeName _) (CExeName _) = True + sameKind (CExeName _) _ = False + sameKind (CTestName _) (CTestName _) = True + sameKind (CTestName _) _ = False + sameKind (CBenchName _) (CBenchName _) = True + sameKind (CBenchName _) _ = False + + setClbis [(CLibName, clbi, _)] = + get >>= \lbi -> + put $ $(recUpdE' (nE "lbi") (mkName "libraryConfig") (AppE (ConE (mkName "Just")) (VarE (mkName "clbi")))) + + setClbis cs@((CExeName _, _, _):_) = + let cfg = (\((CExeName n), clbi, _) -> (n, clbi)) <$> cs in + get >>= \lbi -> + put $ $(recUpdE' (nE "lbi") (mkName "executableConfigs") (VarE $ mkName "cfg")) + + setClbis cs@((CTestName _, _, _):_) = + let cfg = (\((CTestName n), clbi, _) -> (n, clbi)) <$> cs in + get >>= \lbi -> + put $ $(recUpdE' (nE "lbi") (mkName "testSuiteConfigs") (VarE $ mkName "cfg")) + + setClbis cs@((CBenchName _, _, _):_) = + let cfg = (\((CBenchName n), clbi, _) -> (n, clbi)) <$> cs in + get >>= \lbi -> + put $ $(recUpdE' (nE "lbi") (mkName "benchmarkConfigs") (VarE $ mkName "cfg")) + + |]) + + +$(ifD [d| + + componentsConfigs :: + LocalBuildInfo -> [(ComponentName, ComponentLocalBuildInfo, [ComponentName])] + componentsConfigs LocalBuildInfo {..} = + (maybe [] (\c -> [(CLibName, c, [])]) $(nE "libraryConfig")) + ++ ((\(n, clbi) -> (CExeName n, clbi, [])) <$> $(nE "executableConfigs")) + ++ ((\(n, clbi) -> (CTestName n, clbi, [])) <$> $(nE "testSuiteConfigs")) + ++ ((\(n, clbi) -> (CBenchName n, clbi, [])) <$> $(nE "benchmarkConfigs")) + + getComponent :: PackageDescription -> ComponentName -> Component + getComponent pkg cname = + case lookupComponent pkg cname of + Just cpnt -> cpnt + Nothing -> missingComponent + where + missingComponent = + error $ "internal error: the package description contains no " + ++ "component corresponding to " ++ show cname + + lookupComponent :: PackageDescription -> ComponentName -> Maybe Component + lookupComponent pkg CLibName = + fmap CLib $ library pkg + lookupComponent pkg (CExeName name) = + fmap CExe $ find ((name ==) . exeName) (executables pkg) + lookupComponent pkg (CTestName name) = + fmap CTest $ find ((name ==) . testName) (testSuites pkg) + lookupComponent pkg (CBenchName name) = + fmap CBench $ find ((name ==) . benchmarkName) (benchmarks pkg) + +-- We're lying here can't be bothered to order these + allComponentsInBuildOrder :: LocalBuildInfo + -> [(ComponentName, ComponentLocalBuildInfo)] + allComponentsInBuildOrder lbi = + [ (cname, clbi) | (cname, clbi, _) <- componentsConfigs lbi ] + + getComponentLocalBuildInfo :: LocalBuildInfo -> ComponentName -> ComponentLocalBuildInfo + getComponentLocalBuildInfo lbi cname = + case [ clbi + | (cname', clbi, _) <- componentsConfigs lbi + , cname == cname' ] of + [clbi] -> clbi + _ -> missingComponent + where + missingComponent = + error $ "internal error: there is no configuration data " + ++ "for component " ++ show cname + + componentBuildInfo :: Component -> BuildInfo + componentBuildInfo = + foldComponent libBuildInfo buildInfo testBuildInfo benchmarkBuildInfo + + |]) + + +$(ifelsedefD "componentPackageRenaming" [d| + -- M.Map PackageName + newtype Deps = Deps { unDeps :: ([(InstalledPackageId, PackageId)], Map PackageName $(cT "ModuleRenaming")) } +-- $(return $ TySynD $(mkName "Deps") [] [t| |] ) + + noDeps = Deps ([], M.empty) + + getDeps :: ComponentLocalBuildInfo -> Deps + getDeps = componentPackageDeps &&& $(nE "componentPackageRenaming") >>> Deps + + setUnionDeps :: Deps -> ComponentLocalBuildInfo -> ComponentLocalBuildInfo + setUnionDeps (Deps (deps, rns)) clbi = let + clbi' = setComponentPackageRenaming clbi rns + cpdeps = componentPackageDeps clbi + in + clbi' { + componentPackageDeps = cpdeps `union` deps + } + + setComponentPackageRenaming clbi cprn = + -- [| clbi { componentPackageRenaming = componentPackageRenaming clbi `M.union` cprn } |] + $(recUpdE' + (nE "clbi") + (mkName "componentPackageRenaming") + (InfixE + (Just + (AppE + (VarE + (mkName "componentPackageRenaming")) + (VarE (mkName "clbi")) + )) + (VarE (mkName "M.union")) + (Just (VarE (mkName "cprn"))) + ) + ) + + |] [d| + + newtype Deps = Deps { unDeps :: [(InstalledPackageId, PackageId)] } + + noDeps = Deps [] + + getDeps :: ComponentLocalBuildInfo -> Deps + getDeps lbi = Deps $ componentPackageDeps lbi + + setUnionDeps :: Deps -> ComponentLocalBuildInfo -> ComponentLocalBuildInfo + setUnionDeps (Deps deps) clbi = let + cpdeps = componentPackageDeps clbi + in + clbi { + componentPackageDeps = cpdeps `union` deps + } + + +-- setComponentPackageRenaming clbi _cprn = clbi + + |]) diff --git a/Utils.hs b/Utils.hs new file mode 100644 index 0000000..a4c1ff2 --- /dev/null +++ b/Utils.hs @@ -0,0 +1,37 @@ +{-# LANGUAGE CPP #-} +module Utils where + +import Control.Applicative +import Data.Traversable +import System.Directory + +#if MIN_VERSION_directory(1,2,0) +import Data.Time (UTCTime) +#else +import System.Time (ClockTime) +#endif +import Prelude + + +#if MIN_VERSION_directory(1,2,0) +type ModTime = UTCTime +#else +type ModTime = ClockTime +#endif + +data TimedFile = TimedFile { tfPath :: FilePath, tfTime :: ModTime } + deriving (Eq, Show) + +instance Ord TimedFile where + compare (TimedFile _ a) (TimedFile _ b) = compare a b + +timeFile :: FilePath -> IO TimedFile +timeFile f = TimedFile <$> pure f <*> getModificationTime f + +mightExist :: FilePath -> IO (Maybe FilePath) +mightExist f = do + exists <- doesFileExist f + return $ if exists then (Just f) else (Nothing) + +timeMaybe :: FilePath -> IO (Maybe TimedFile) +timeMaybe f = traverse timeFile =<< mightExist f diff --git a/doc/emacs.piki b/doc/emacs.piki index 31171d5..31643a5 100644 --- a/doc/emacs.piki +++ b/doc/emacs.piki @@ -119,7 +119,7 @@ foo xs = foldr bar id xs bar = (:) |< -C-xC-s highlights the 2nd line. C-c? displays the following: +C-xC-s highlights the 2nd line. M-? displays the following: >| Couldn't match type `[a -> a]' with `a -> a' @@ -139,7 +139,7 @@ foo xs = foldr _bar id xs bar = (:) |< -C-c? displays: +M-? displays: >| Found hole `_bar' with type: (a -> a) -> (a -> a) -> a -> a diff --git a/elisp/ghc-check.el b/elisp/ghc-check.el index eafadfa..37d02fc 100644 --- a/elisp/ghc-check.el +++ b/elisp/ghc-check.el @@ -20,7 +20,7 @@ :underline (:style wave :color "orangered")) (t :inherit error)) - "Face used for marking error lines." + "Face used for error lines." :group 'ghc) (defface ghc-face-warn @@ -28,7 +28,7 @@ :underline (:style wave :color "gold")) (t :inherit warning)) - "Face used for marking warning lines." + "Face used for warning lines." :group 'ghc) (defface ghc-face-hole @@ -36,7 +36,7 @@ :underline (:style wave :color "purple")) (t :inherit warning)) - "Face used for marking hole lines." + "Face used for hole lines." :group 'ghc) (defvar ghc-check-error-fringe (propertize "!" 'display '(left-fringe exclamation-mark))) @@ -46,27 +46,34 @@ (defvar ghc-check-hole-fringe (propertize "_" 'display '(left-fringe horizontal-bar))) (defvar ghc-display-error nil - "*An action to display errors/warnings for 'M-n' and 'M-p: + "*How to display errors/warnings when using 'M-n' and 'M-p': -nil does not display errors/warnings. -'minibuffer displays errors/warnings in the minibuffer. -'other-buffer displays errors/warnings in the other buffer. +nil do not display errors/warnings. +'minibuffer display errors/warnings in the minibuffer. +'other-buffer display errors/warnings in a new buffer. ") (defvar ghc-display-hole 'other-buffer - "*An action to display hole information for 'C-c C-j' and 'C-c C-h' + "*How to display hole information when using 'C-c C-j' and 'C-c C-h' -'minibuffer displays errors/warnings in the minibuffer. -'other-buffer displays errors/warnings in the other buffer" +'minibuffer display errors/warnings in the minibuffer. +'other-buffer display errors/warnings in the a new buffer" ) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defun ghc-check-syntax () (interactive) - (ghc-with-process (ghc-check-send) - 'ghc-check-callback - (lambda () (setq mode-line-process " -:-")))) + ;; Only check syntax of visible buffers + (when (and (buffer-file-name) + (file-exists-p (buffer-file-name)) + (get-buffer-window (current-buffer) t)) + (with-timeout + (10 (error "ghc process may have hung or exited with an error")) + (while ghc-process-running (sleep-for 0.1))) + (ghc-with-process (ghc-check-send) + 'ghc-check-callback + (lambda () (setq mode-line-process " -:-"))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -132,7 +139,7 @@ nil does not display errors/warnings. info infos) (dolist (err errs (nreverse infos)) (when (string-match regex err) - (let* ((file (expand-file-name (match-string 1 err))) ;; for Windows + (let* ((file (expand-file-name (match-string 1 err) ghc-process-root)) ;; for Windows (line (string-to-number (match-string 2 err))) (coln (string-to-number (match-string 3 err))) (msg (match-string 4 err)) @@ -167,18 +174,20 @@ nil does not display errors/warnings. ;; If this is a bottleneck for a large code, let's fix. (goto-char (point-min)) (cond - ((and (string= ofile file) hole) - (forward-line (1- line)) - (forward-char (1- coln)) - (setq beg (point)) - (forward-char (length hole)) - (setq end (point))) - ((string= ofile file) - (forward-line (1- line)) - (while (eq (char-after) 32) (forward-char)) - (setq beg (point)) - (forward-line) - (setq end (1- (point)))) + ((string= (file-truename ofile) (file-truename file)) + (if hole + (progn + (forward-line (1- line)) + (forward-char (1- coln)) + (setq beg (point)) + (forward-char (length hole)) + (setq end (point))) + (progn + (forward-line (1- line)) + (forward-char (1- coln)) + (setq beg (point)) + (skip-chars-forward "^[:space:]" (line-end-position)) + (setq end (point))))) (t (setq beg (point)) (forward-line) diff --git a/elisp/ghc-comp.el b/elisp/ghc-comp.el index 2b26a16..2209ca2 100644 --- a/elisp/ghc-comp.el +++ b/elisp/ghc-comp.el @@ -127,7 +127,7 @@ unloaded modules are loaded") (interactive) (if (ghc-should-scroll) (ghc-scroll-completion-buffer) - (ghc-try-complete))) + (ghc-try-complete))) (defun ghc-should-scroll () (let ((window (ghc-completion-window))) diff --git a/elisp/ghc-doc.el b/elisp/ghc-doc.el index 46fe2aa..9c61125 100644 --- a/elisp/ghc-doc.el +++ b/elisp/ghc-doc.el @@ -25,7 +25,7 @@ (setq pkg-ver-path (and mod (ghc-resolve-document-path mod))) (if pkg-ver-path (ghc-display-document pkg-ver-path mod haskell-org expr) - (message "No document found")))) + (message "No documentation found")))) (ghc-defstruct pkg-ver-path pkg ver path) @@ -93,7 +93,7 @@ (read-from-minibuffer "Module name: " def ghc-input-map)) (defun ghc-read-expression (def) - (read-from-minibuffer "Expression: " def ghc-input-map)) + (read-from-minibuffer "Identifier: " def ghc-input-map)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/elisp/ghc-func.el b/elisp/ghc-func.el index 97f749b..83d1840 100644 --- a/elisp/ghc-func.el +++ b/elisp/ghc-func.el @@ -182,9 +182,19 @@ (funcall ins-func) (goto-char (point-min)) (if (not fontify) - (turn-off-haskell-font-lock) + ;; turn-off-haskell-font-lock has been removed from haskell-mode + ;; test if the function is defined in our version + (if (fboundp 'turn-off-haskell-font-lock) + (turn-off-haskell-font-lock) + ;; it's not defined, fallback on font-lock-mode + (font-lock-mode -1)) (haskell-font-lock-defaults-create) - (turn-on-haskell-font-lock))) + ;; turn-on-haskell-font-lock has been removed from haskell-mode + ;; test if the function is defined in our version + (if (fboundp 'turn-on-haskell-font-lock) + (turn-on-haskell-font-lock) + ;; it's not defined, fallback on font-lock-mode + (turn-on-font-lock)))) (display-buffer buf '((display-buffer-reuse-window display-buffer-pop-up-window)))))) diff --git a/elisp/ghc-info.el b/elisp/ghc-info.el index acfbb05..d7854c5 100644 --- a/elisp/ghc-info.el +++ b/elisp/ghc-info.el @@ -82,7 +82,7 @@ (if (null tinfos) (progn (ghc-type-clear-overlay) - (message "Cannot guess type")) + (message "Cannot determine type")) (let* ((tinfo (nth (ghc-type-get-ix) tinfos)) (type (ghc-tinfo-get-info tinfo)) (beg-line (ghc-tinfo-get-beg-line tinfo)) @@ -127,7 +127,7 @@ (defun ghc-expand-th () (interactive) (let* ((file (buffer-file-name)) - (cmds (list "expand" file)) + (cmds (list "-b" "\n" "expand" file)) (source (ghc-run-ghc-mod cmds))) (when source (ghc-display diff --git a/elisp/ghc-pkg.el b/elisp/ghc-pkg.el index 62e8c2e..79e57b8 100644 --- a/elisp/ghc-pkg.el +++ b/elisp/ghc-pkg.el @@ -2,4 +2,4 @@ "ghc" 2.0.0 "Sub mode for Haskell mode" - nil) + '((haskell-mode "13.0"))) diff --git a/elisp/ghc-process.el b/elisp/ghc-process.el index 00aed43..497ecde 100644 --- a/elisp/ghc-process.el +++ b/elisp/ghc-process.el @@ -10,6 +10,9 @@ (require 'ghc-func) +(defvar ghc-debug-options nil) +;; (setq ghc-debug-options '("-v9")) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defvar ghc-process-running nil) @@ -19,8 +22,11 @@ (defvar-local ghc-process-original-file nil) (defvar-local ghc-process-callback nil) (defvar-local ghc-process-hook nil) +(defvar-local ghc-process-root nil) -(defvar ghc-interactive-command "ghc-modi") +(defvar ghc-command "ghc-mod") + +(defvar ghc-error-buffer "*GHC Error*") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -28,28 +34,30 @@ (ghc-run-ghc-mod '("root"))) (defun ghc-with-process (cmd callback &optional hook1 hook2) - (unless ghc-process-process-name - (setq ghc-process-process-name (ghc-get-project-root))) - (when (and ghc-process-process-name (not ghc-process-running)) - (setq ghc-process-running t) - (if hook1 (funcall hook1)) - (let* ((cbuf (current-buffer)) - (name ghc-process-process-name) - (buf (get-buffer-create (concat " ghc-modi:" name))) - (file (buffer-file-name)) - (cpro (get-process name))) - (ghc-with-current-buffer buf - (setq ghc-process-original-buffer cbuf) - (setq ghc-process-original-file file) - (setq ghc-process-callback callback) - (setq ghc-process-hook hook2) - (erase-buffer) - (let ((pro (ghc-get-process cpro name buf))) - (process-send-string pro cmd) - (when ghc-debug - (ghc-with-debug-buffer - (insert (format "%% %s" cmd)))) - pro))))) + (let ((root (ghc-get-project-root))) + (unless ghc-process-process-name + (setq ghc-process-process-name root)) + (when (and ghc-process-process-name (not ghc-process-running)) + (setq ghc-process-running t) + (if hook1 (funcall hook1)) + (let* ((cbuf (current-buffer)) + (name ghc-process-process-name) + (buf (get-buffer-create (concat " ghc-mod:" name))) + (file (buffer-file-name)) + (cpro (get-process name))) + (ghc-with-current-buffer buf + (setq ghc-process-original-buffer cbuf) + (setq ghc-process-original-file file) + (setq ghc-process-callback callback) + (setq ghc-process-hook hook2) + (setq ghc-process-root root) + (erase-buffer) + (let ((pro (ghc-get-process cpro name buf))) + (process-send-string pro cmd) + (when ghc-debug + (ghc-with-debug-buffer + (insert (format "%% %s" cmd)))) + pro)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -63,37 +71,74 @@ (t cpro))) (defun ghc-start-process (name buf) - (let* ((opts (append '("-b" "\n" "-l") (ghc-make-ghc-options))) - (pro (apply 'start-file-process name buf ghc-interactive-command opts))) + (let* ((opts (append ghc-debug-options + '("-b" "\n" "-l" "--line-prefix=O: ,E: ") + (ghc-make-ghc-options) + '("legacy-interactive"))) + (pro (apply 'start-file-process name buf ghc-command opts))) (set-process-filter pro 'ghc-process-filter) (set-process-sentinel pro 'ghc-process-sentinel) (set-process-query-on-exit-flag pro nil) pro)) (defun ghc-process-filter (process string) - (let ((pbuf (process-buffer process))) + (let* ((pbuf (process-buffer process)) + (tbufname (concat " tmp " (buffer-name pbuf))) + tbuf) (if (not (get-buffer pbuf)) (setq ghc-process-running nil) ;; just in case - (ghc-with-current-buffer (process-buffer process) - (goto-char (point-max)) - (insert string) + (ghc-with-current-buffer pbuf + (when ghc-debug + (ghc-with-debug-buffer + (insert string))) + (with-current-buffer (get-buffer-create tbufname) + (setq tbuf (current-buffer)) + (goto-char (point-max)) + (insert string) + (goto-char (point-min)) + (let ((cont t) end out) + (while (and cont (not (eobp))) + (cond + ((looking-at "^O: ") + (setq out t)) + ((looking-at "^E: ") + (setq out nil)) + (t + (setq cont nil))) + (when cont + (forward-line) + (unless (bolp) (setq cont nil))) + (when cont + (delete-region 1 4) + (setq end (point)) + (if out + (with-current-buffer pbuf + (goto-char (point-max)) + (insert-buffer-substring tbuf 1 end)) + (with-current-buffer (get-buffer-create ghc-error-buffer) + (setq buffer-read-only t) + (let* ((buffer-read-only nil) + (inhibit-read-only t) + (cbuf (current-buffer)) + cwin) + (unless (get-buffer-window cbuf) (display-buffer cbuf)) + (setq cwin (get-buffer-window cbuf)) + (with-selected-window cwin + (goto-char (point-max)) + (insert-buffer-substring tbuf 1 end) + (set-buffer-modified-p nil) + (redisplay))))) + (delete-region 1 end))))) + (goto-char (point-max)) (forward-line -1) (cond ((looking-at "^OK$") (if ghc-process-hook (funcall ghc-process-hook)) (goto-char (point-min)) (funcall ghc-process-callback 'ok) - (when ghc-debug - (let ((cbuf (current-buffer))) - (ghc-with-debug-buffer - (insert-buffer-substring cbuf)))) (setq ghc-process-running nil)) ((looking-at "^NG ") (funcall ghc-process-callback 'ng) - (when ghc-debug - (let ((cbuf (current-buffer))) - (ghc-with-debug-buffer - (insert-buffer-substring cbuf)))) (setq ghc-process-running nil))))))) (defun ghc-process-sentinel (process event) diff --git a/elisp/ghc.el b/elisp/ghc.el index 6da90b9..a555c47 100644 --- a/elisp/ghc.el +++ b/elisp/ghc.el @@ -28,7 +28,7 @@ (< emacs-minor-version minor))) (error "ghc-mod requires at least Emacs %d.%d" major minor))) -(defconst ghc-version "5.2.1.2") +(defconst ghc-version "5.3.0.0") ;; (eval-when-compile ;; (require 'haskell-mode)) @@ -117,6 +117,9 @@ (setq ghc-initialized t) (defadvice save-buffer (after ghc-check-syntax-on-save activate) "Check syntax with GHC when a haskell-mode buffer is saved." + (when (eq 'haskell-mode major-mode) (ghc-check-syntax))) + (defadvice switch-to-buffer (after ghc-check-syntax-on-switch-to-buffer activate) + "Check syntax with GHC when switching to a haskell-mode buffer." (when (eq 'haskell-mode major-mode) (ghc-check-syntax)))) (ghc-import-module) (ghc-check-syntax)) @@ -130,23 +133,19 @@ (let ((el-path (locate-file "ghc.el" load-path)) (ghc-path (executable-find "ghc")) ;; FIXME (ghc-mod-path (executable-find ghc-module-command)) - (ghc-modi-path (executable-find ghc-interactive-command)) (el-ver ghc-version) (ghc-ver (ghc-run-ghc-mod '("--version") "ghc")) (ghc-mod-ver (ghc-run-ghc-mod '("version"))) - (ghc-modi-ver (ghc-run-ghc-mod '("version") ghc-interactive-command)) (path (getenv "PATH"))) (switch-to-buffer (get-buffer-create "**GHC Debug**")) (erase-buffer) (insert "Path: check if you are using intended programs.\n") (insert (format "\t ghc.el path: %s\n" el-path)) (insert (format "\t ghc-mod path: %s\n" ghc-mod-path)) - (insert (format "\tghc-modi path: %s\n" ghc-modi-path)) (insert (format "\t ghc path: %s\n" ghc-path)) - (insert "\nVersion: all versions must be the same.\n") + (insert "\nVersion: all GHC versions must be the same.\n") (insert (format "\t ghc.el version %s\n" el-ver)) (insert (format "\t %s\n" ghc-mod-ver)) - (insert (format "\t%s\n" ghc-modi-ver)) (insert (format "\t%s\n" ghc-ver)) (insert "\nEnvironment variables:\n") (insert (format "\tPATH=%s\n" path)))) diff --git a/ghc-mod.cabal b/ghc-mod.cabal index 3697e97..40cdde3 100644 --- a/ghc-mod.cabal +++ b/ghc-mod.cabal @@ -1,73 +1,102 @@ Name: ghc-mod -Version: 5.2.1.2 -Author: Kazu Yamamoto - Daniel Gröber +Version: 5.3.0.0 +Author: Kazu Yamamoto , + Daniel Gröber , Alejandro Serrano -Maintainer: Kazu Yamamoto -License: BSD3 +Maintainer: Daniel Gröber +License: AGPL-3 License-File: LICENSE +License-Files: COPYING.BSD3 COPYING.AGPL3 Homepage: http://www.mew.org/~kazu/proj/ghc-mod/ Synopsis: Happy Haskell Programming -Description: The ghc-mod command is a backend command to enrich - Haskell programming on editors including - Emacs, Vim, and Sublime. - The ghc-mod command is based on ghc-mod library - which is a wrapper of GHC API. - This package includes the ghc-mod command, - the ghc-mod library, and Emacs front-end - (for historical reasons). - For more information, please see its home page. +Description: + ghc-mod is a backend program to enrich Haskell programming in editors. It + strives to offer most of the features one has come to expect from modern IDEs + in any editor. -Category: Development -Cabal-Version: >= 1.10 -Build-Type: Simple -Data-Dir: elisp -Data-Files: Makefile ghc.el ghc-func.el ghc-doc.el ghc-comp.el - ghc-check.el ghc-process.el ghc-command.el ghc-info.el - ghc-ins-mod.el ghc-indent.el ghc-pkg.el ghc-rewrite.el + ghc-mod provides a library for other haskell programs to use as well as a + standalone program for easy editor integration. All of the fundamental + functionality of the frontend program can be accessed through the library + however many implementation details are hidden and if you want to + significantly extend ghc-mod you should submit these changes upstream instead + of implementing them on top of the library. + + For more information, please see its home page. + +Category: GHC, Development +Cabal-Version: >= 1.14 +Build-Type: Custom +Data-Files: elisp/Makefile + elisp/*.el +Data-Files: LICENSE COPYING.BSD3 COPYING.AGPL3 Extra-Source-Files: ChangeLog - test/data/*.cabal - test/data/*.hs - test/data/cabal.sandbox.config.in - test/data/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d/Cabal-1.18.1.3-2b161c6bf77657aa17e1681d83cb051b.conf + SetupCompat.hs + NotCPP/*.hs + test/data/annotations/*.hs test/data/broken-cabal/*.cabal test/data/broken-cabal/cabal.sandbox.config.in - test/data/broken-sandbox/*.cabal test/data/broken-sandbox/cabal.sandbox.config + test/data/broken-sandbox/dummy.cabal + test/data/cabal-flags/cabal-flags.cabal + test/data/cabal-project/*.cabal + test/data/cabal-project/*.hs + test/data/cabal-project/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d/Cabal-1.18.1.3-2b161c6bf77657aa17e1681d83cb051b.conf + test/data/cabal-project/cabal.sandbox.config.in + test/data/cabal-project/subdir1/subdir2/dummy test/data/case-split/*.hs - test/data/cabal-flags/*.cabal + test/data/check-packageid/cabal.sandbox.config.in + test/data/check-packageid/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d/template-haskell-2.8.0.0-32d4f24abdbb6bf41272b183b2e23e9c.conf test/data/check-test-subdir/*.cabal test/data/check-test-subdir/src/Check/Test/*.hs test/data/check-test-subdir/test/*.hs test/data/check-test-subdir/test/Bar/*.hs - test/data/check-packageid/cabal.sandbox.config.in - test/data/check-packageid/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d/template-haskell-2.8.0.0-32d4f24abdbb6bf41272b183b2e23e9c.conf test/data/duplicate-pkgver/cabal.sandbox.config.in test/data/duplicate-pkgver/duplicate-pkgver.cabal test/data/duplicate-pkgver/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d/template-haskell-1.0-7c59d13f32294d1ef6dc6233c24df961.conf test/data/duplicate-pkgver/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d/template-haskell-2.8.0.0-14e543bdae2da4d2aeff5386892c9112.conf test/data/duplicate-pkgver/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d/template-haskell-2.8.0.0-32d4f24abdbb6bf41272b183b2e23e9c.conf - test/data/pattern-synonyms/*.cabal - test/data/pattern-synonyms/*.hs + test/data/foreign-export/*.hs test/data/ghc-mod-check/*.cabal test/data/ghc-mod-check/*.hs - test/data/ghc-mod-check/Data/*.hs - test/data/subdir1/subdir2/dummy - test/data/.cabal-sandbox/packages/00-index.tar + test/data/ghc-mod-check/lib/Data/*.hs + test/data/hlint/*.hs + test/data/home-module-graph/cpp/*.hs + test/data/home-module-graph/cycle/*.hs + test/data/home-module-graph/errors/*.hs + test/data/home-module-graph/indirect/*.hs + test/data/home-module-graph/indirect-update/*.hs + test/data/import-cycle/*.hs + test/data/non-exported/*.hs + test/data/pattern-synonyms/*.cabal + test/data/pattern-synonyms/*.hs + test/data/quasi-quotes/*.hs + test/data/template-haskell/*.hs + test/data/target/*.hs + test/data/check-missing-warnings/*.hs + test/data/custom-cradle/custom-cradle.cabal + test/data/custom-cradle/ghc-mod.package-db-stack + test/data/custom-cradle/package-db-a/.gitkeep + test/data/custom-cradle/package-db-b/.gitkeep + test/data/custom-cradle/package-db-c/.gitkeep + test/data/cabal-preprocessors/*.cabal + test/data/cabal-preprocessors/*.hs + test/data/cabal-preprocessors/*.hsc Library Default-Language: Haskell2010 - GHC-Options: -Wall - Default-Extensions: ConstraintKinds, FlexibleContexts + GHC-Options: -Wall -fno-warn-deprecations + Default-Extensions: ScopedTypeVariables, RecordWildCards, NamedFieldPuns, + ConstraintKinds, FlexibleContexts, + DataKinds, KindSignatures, TypeOperators Exposed-Modules: Language.Haskell.GhcMod Language.Haskell.GhcMod.Internal - Other-Modules: Language.Haskell.GhcMod.Boot + Other-Modules: Paths_ghc_mod + Utils + Language.Haskell.GhcMod.Boot Language.Haskell.GhcMod.Browse - Language.Haskell.GhcMod.Cabal16 - Language.Haskell.GhcMod.Cabal18 - Language.Haskell.GhcMod.Cabal21 - Language.Haskell.GhcMod.CabalApi - Language.Haskell.GhcMod.CabalConfig + Language.Haskell.GhcMod.CabalHelper + Language.Haskell.GhcMod.Caching + Language.Haskell.GhcMod.Caching.Types Language.Haskell.GhcMod.CaseSplit Language.Haskell.GhcMod.Check Language.Haskell.GhcMod.Convert @@ -79,18 +108,21 @@ Library Language.Haskell.GhcMod.FillSig Language.Haskell.GhcMod.Find Language.Haskell.GhcMod.Flag - Language.Haskell.GhcMod.GHCApi - Language.Haskell.GhcMod.GHCChoice Language.Haskell.GhcMod.Gap Language.Haskell.GhcMod.GhcPkg + Language.Haskell.GhcMod.HomeModuleGraph Language.Haskell.GhcMod.Info Language.Haskell.GhcMod.Lang Language.Haskell.GhcMod.Lint Language.Haskell.GhcMod.Logger + Language.Haskell.GhcMod.Logging Language.Haskell.GhcMod.Modules Language.Haskell.GhcMod.Monad + Language.Haskell.GhcMod.Monad.Types + Language.Haskell.GhcMod.Output Language.Haskell.GhcMod.PathsAndFiles Language.Haskell.GhcMod.PkgDoc + Language.Haskell.GhcMod.Pretty Language.Haskell.GhcMod.Read Language.Haskell.GhcMod.SrcUtils Language.Haskell.GhcMod.Target @@ -98,7 +130,10 @@ Library Language.Haskell.GhcMod.Utils Language.Haskell.GhcMod.World Build-Depends: base >= 4.0 && < 5 + , bytestring + , cereal >= 0.4 , containers + , cabal-helper == 0.5.* && >= 0.5.1.0 , deepseq , directory , filepath @@ -106,7 +141,6 @@ Library , ghc-paths , ghc-syb-utils , hlint >= 1.8.61 - , io-choice , monad-journal >= 0.4 , old-time , pretty @@ -117,30 +151,28 @@ Library , transformers , transformers-base , mtl >= 2.0 - , monad-control + , monad-control >= 1 , split , haskell-src-exts , text , djinn-ghc >= 0.0.2.2 - if impl(ghc >= 7.8) - Build-Depends: Cabal >= 1.18 - else + , fclabels + if impl(ghc < 7.8) Build-Depends: convertible - , Cabal >= 1.10 && < 1.17 - if impl(ghc <= 7.4.2) + if impl(ghc < 7.5) -- Only used to constrain random to a version that still works with GHC 7.4 - Build-Depends: random <= 1.0.1.1 + Build-Depends: random <= 1.0.1.1, + ghc-prim Executable ghc-mod Default-Language: Haskell2010 Main-Is: GHCMod.hs Other-Modules: Paths_ghc_mod - GHC-Options: -Wall + GHC-Options: -Wall -fno-warn-deprecations Default-Extensions: ConstraintKinds, FlexibleContexts HS-Source-Dirs: src Build-Depends: base >= 4.0 && < 5 , async - , data-default , directory , filepath , pretty @@ -156,22 +188,17 @@ Executable ghc-modi Other-Modules: Paths_ghc_mod Misc Utils - GHC-Options: -Wall -threaded + GHC-Options: -Wall -threaded -fno-warn-deprecations if os(windows) Cpp-Options: -DWINDOWS Default-Extensions: ConstraintKinds, FlexibleContexts - HS-Source-Dirs: src + HS-Source-Dirs: src, . Build-Depends: base >= 4.0 && < 5 - , async - , containers , directory , filepath - , old-time , process - , split , time - , ghc - , ghc-mod + , old-time Test-Suite doctest Type: exitcode-stdio-1.0 @@ -180,20 +207,27 @@ Test-Suite doctest Ghc-Options: -Wall Default-Extensions: ConstraintKinds, FlexibleContexts Main-Is: doctests.hs + if impl(ghc == 7.4.*) + Buildable: False Build-Depends: base , doctest >= 0.9.3 Test-Suite spec Default-Language: Haskell2010 - Default-Extensions: ConstraintKinds, FlexibleContexts + Default-Extensions: ScopedTypeVariables, RecordWildCards, NamedFieldPuns, + ConstraintKinds, FlexibleContexts, + DataKinds, KindSignatures, TypeOperators Main-Is: Main.hs Hs-Source-Dirs: test, . - Ghc-Options: -Wall + Ghc-Options: -Wall -fno-warn-deprecations + CPP-Options: -DSPEC=1 Type: exitcode-stdio-1.0 - Other-Modules: BrowseSpec - CabalApiSpec - CheckSpec + Other-Modules: Paths_ghc_mod Dir + Spec + TestUtils + BrowseSpec + CheckSpec FlagSpec InfoSpec LangSpec @@ -201,42 +235,14 @@ Test-Suite spec ListSpec MonadSpec PathsAndFilesSpec - Spec - TestUtils - Build-Depends: base >= 4.0 && < 5 - , containers - , deepseq - , directory - , filepath - , ghc - , ghc-paths - , ghc-syb-utils - , hlint >= 1.7.1 - , io-choice - , monad-journal >= 0.4 - , old-time - , pretty - , process - , syb - , temporary - , time - , transformers - , transformers-base - , mtl >= 2.0 - , monad-control - , hspec >= 1.8.2 - , split - , haskell-src-exts - , text - , djinn-ghc >= 0.0.2.2 - if impl(ghc >= 7.8) - Build-Depends: Cabal >= 1.18 - else - Build-Depends: convertible - , Cabal >= 1.10 && < 1.17 - if impl(ghc < 7.6) - Build-Depends: executable-path - CPP-Options: -DSPEC=1 + HomeModuleGraphSpec + + Build-Depends: hspec >= 2.0.0 + if impl(ghc == 7.4.*) + Build-Depends: executable-path + X-Build-Depends-Like: CLibName + + Source-Repository head Type: git diff --git a/ghcmodHappyHaskellProgram-Dg.tex b/ghcmodHappyHaskellProgram-Dg.tex new file mode 100644 index 0000000..7af3027 --- /dev/null +++ b/ghcmodHappyHaskellProgram-Dg.tex @@ -0,0 +1,51 @@ +% ghcmodHappyHaskellProgram-Dg.tex +\begin{hcarentry}[updated]{ghc-mod --- Happy Haskell Programming} +\report{Daniel Gr\"ober}%05/15 +\status{open source, actively developed} +\makeheader + +\texttt{ghc-mod} is both a backend program for enhancing editors and other kinds +of development environments with support for Haskell, and an Emacs package +providing the user facing functionality, internally called \texttt{ghc} for +historical reasons. Other people have also developed numerous front ends for Vim +and there also exist some for Atom and a few other proprietary editors. + +After a period of declining activity, development has been picking up pace again +since Daniel Gr\"ober took over as maintainer. Most changes during versions +5.0.0--5.2.1.2 consisted only of fixes and internal cleanup work, but for the +past four months, vastly improved Cabal support has been in the works and is now +starting to stabilize. + +This work is a major step forward in terms of how well ghc-mod's suggestions +reflect what \texttt{cabal build} would report, and should also allow ghc-mod's +other features to work even in more complicated Cabal setups. + +Daniel Gr\"ober has been accepted for a summer internship at IIJ Innovation +Institute's Research Laboratory working on \texttt{ghc-mod} for two months +(August--September). He will be working on: +\begin{compactitem} + + \item adding GHCi-like interactive code execution, to bring \texttt{ghc-mod} up + to feature parity with GHCi and beyond, + + \item investigating how to best cooperate with \texttt{ide-backend}, + + \item adding a network interface to make using ghc-mod in other projects + easier, and + + \item if time allows, cleaning up the Emacs frontend to be more user-friendly + and in line with Emacs' conventions. +\end{compactitem} + +The goal of this work is to make \texttt{ghc-mod} the obvious choice for anyone +implementing Haskell support for a development environment and improving +\texttt{ghc-mod}'s overall feature set and reliability in order to give new as +well as experienced Haskell developers the best possible experience. + +Right now \texttt{ghc-mod} has only one core developer and only a handful of +occasional drive-by contributors. If \textit{you} want to help make Haskell +development even more fun come and join us! + +\FurtherReading + \url{https://github.com/kazu-yamamoto/ghc-mod} +\end{hcarentry} diff --git a/hcar-ghc-mod.tex b/hcar-ghc-mod.tex deleted file mode 100644 index ea738ca..0000000 --- a/hcar-ghc-mod.tex +++ /dev/null @@ -1,26 +0,0 @@ -% ghcmodHappyHaskellProgram-Kg.tex -\begin{hcarentry}[updated]{ghc-mod --- Happy Haskell Programming} -\report{Kazu Yamamoto}%11/14 -\status{open source, actively developed} -\makeheader - -For a long time, Kazu Yamamoto was the only active developer of ghc-mod, now two -new developers have joined: - -Alejandro Serrano merged the results of his Google Summer of Code project. He -implemented case splitting and sophisticated typed hole handling. Daniel Gröber -brushed up the internal code and introduced the GhcModT monad now used -throughout the exported API. As a result the API of \texttt{ghc-mod} drastically -changed with version 5.0.0. - -\texttt{ghc-modi} used to suffer from various consistency related issues -triggered by changes in the environment, for instance: changing file names of -modules, adding dependencies to the cabal file and installing new libraries. -\texttt{ghc-modi} v5.1.1 or later handles changes in the environment by -restarting the GHC session when this is detected. - -Kazu stepped down as release manager and Daniel took over. - -\FurtherReading - \url{http://www.mew.org/~kazu/proj/ghc-mod/en/} -\end{hcarentry} diff --git a/scripts/bump.sh b/scripts/bump.sh index 445622e..986b46b 100755 --- a/scripts/bump.sh +++ b/scripts/bump.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -e + if [ -z "$1" ]; then echo "Usage: $0 VERSION" >&2 exit 1 @@ -19,6 +21,13 @@ sed -i 's/(defconst ghc-version ".*")/(defconst ghc-version "'"$VERSION"'")/' \ sed -r -i 's/^(Version:[[:space:]]*)[0-9.]+/\1'"$VERSION"'/' ghc-mod.cabal +git add elisp/ghc.el ghc-mod.cabal +git commit -m "Bump version to $VERSION" + +git checkout release +#git merge master +git merge -s recursive -X theirs master + ( tac ChangeLog; echo "\n$(date '+%Y-%m-%d') v$VERSION" ) | tac \ > ChangeLog.tmp @@ -26,6 +35,8 @@ mv ChangeLog.tmp ChangeLog emacs -q -nw ChangeLog -git add ChangeLog elisp/ghc.el ghc-mod.cabal -git commit -m "Bump version to $VERSION" +git add ChangeLog +git commit -m "ChangeLog" + + git tag "v$VERSION" diff --git a/scripts/compare-versions.sh b/scripts/compare-versions.sh new file mode 100644 index 0000000..a8a979b --- /dev/null +++ b/scripts/compare-versions.sh @@ -0,0 +1,36 @@ +################################################################################ +# # +# Find version differences in common packages of `ghc-pkg list` dumps. # +# # +# Copyright (C) 2015 Daniel Gröber # +# # +# Copying and distribution of this file, with or without modification, # +# are permitted in any medium without royalty provided the copyright # +# notice and this notice are preserved. This file is offered as-is, # +# without any warranty. # +# # +# Usage: sh compare-versions.sh FILE1 FILE2 # +# # +# Example: # +# sh compare-versions.sh =(ghc-pkg list) =(ssh some-host ghc-pkg list) # +# # +# Where `=(command)` is equivalent to: # +# `(tmp=$(mktemp); command > $tmp; echo $tmp)` # +# # +# # +# The output consists of lines in the format: # +# # +# VERSION1 is the version from FILE1 and VERSION2 is the version from FILE2 # +# # +################################################################################ + +t1=$(mktemp) +t2=$(mktemp) + +grep "^ " "$1" | sed 's/ *\(.*\)-\([0-9.]\+\)/\1 \2/' | sort > $t1 +grep "^ " "$2" | sed 's/ *\(.*\)-\([0-9.]\+\)/\1 \2/' | sort > $t2 + +comm -3 -2 $t1 $t2 | sort -k 1b,1 > $t1.diff +comm -3 -1 $t1 $t2 | sort -k 1b,1 > $t2.diff + +join $t1.diff $t2.diff | sort | uniq diff --git a/src/GHCMod.hs b/src/GHCMod.hs index 1b5bad2..46d858d 100644 --- a/src/GHCMod.hs +++ b/src/GHCMod.hs @@ -6,41 +6,40 @@ import Config (cProjectVersion) import MonadUtils (liftIO) import Control.Applicative import Control.Monad -import Control.Exception ( SomeException(..), fromException, Exception - , Handler(..), catches, throw) import Data.Typeable (Typeable) import Data.Version (showVersion) -import Data.Default import Data.List import Data.List.Split -import Data.Maybe import Data.Char (isSpace) +import Data.Maybe +import Exception import Language.Haskell.GhcMod -import Language.Haskell.GhcMod.Internal +import Language.Haskell.GhcMod.Internal hiding (MonadIO,liftIO) import Paths_ghc_mod import System.Console.GetOpt (OptDescr(..), ArgDescr(..), ArgOrder(..)) import qualified System.Console.GetOpt as O -import System.Directory (setCurrentDirectory) -import System.Environment (getArgs,getProgName) +import System.FilePath (()) +import System.Directory (setCurrentDirectory, getAppUserDataDirectory, + removeDirectoryRecursive) +import System.Environment (getArgs) import System.Exit (exitFailure) -import System.IO (hPutStrLn, stdout, stderr, hSetEncoding, utf8, hFlush) -import System.IO.Unsafe (unsafePerformIO) -import System.FilePath (takeFileName) -import System.Exit (ExitCode, exitSuccess) +import System.IO (stdout, hSetEncoding, utf8, hFlush) +import System.Exit (exitSuccess) import Text.PrettyPrint +import Prelude import Misc +progVersion :: String -> String +progVersion pf = + "ghc-mod"++pf++" version " ++ showVersion version ++ " compiled by GHC " + ++ cProjectVersion ++ "\n" +ghcModVersion :: String +ghcModVersion = progVersion "" -progVersion :: String -progVersion = - progName ++ " version " ++ showVersion version ++ " compiled by GHC " - ++ cProjectVersion ++ "\n" - --- TODO: remove (ghc) version prefix! -progName :: String -progName = unsafePerformIO $ takeFileName <$> getProgName +ghcModiVersion :: String +ghcModiVersion = progVersion "i" optionUsage :: (String -> String) -> [OptDescr a] -> [String] optionUsage indent opts = concatMap optUsage opts @@ -65,33 +64,27 @@ optionUsage indent opts = concatMap optUsage opts ReqArg _ label -> s ++ label OptArg _ label -> s ++ "["++label++"]" +-- TODO: Generate the stuff below automatically usage :: String usage = - case progName of - "ghc-modi" -> ghcModiUsage - _ -> ghcModUsage - --- TODO: Generate the stuff below automatically -ghcModUsage :: String -ghcModUsage = - "Usage: ghc-mod [OPTIONS...] COMMAND [OPTIONS...] \n\ + "Usage: ghc-mod [OPTIONS...] COMMAND [CMD_ARGS...] \n\ \*Global Options (OPTIONS)*\n\ \ Global options can be specified before and after the command and\n\ \ interspersed with command specific options\n\ \\n" ++ (unlines $ indent <$> optionUsage indent globalArgSpec) ++ "*Commands*\n\ - \ - version | --version\n\ + \ - version\n\ \ Print the version of the program.\n\ \\n\ - \ - help | --help\n\ + \ - help\n\ \ Print this help message.\n\ \\n\ \ - list [FLAGS...] | modules [FLAGS...]\n\ \ List all visible modules.\n\ \ Flags:\n\ \ -d\n\ - \ Also print the modules' package.\n\ + \ Print package modules belong to.\n\ \\n\ \ - lang\n\ \ List all known GHC language extensions.\n\ @@ -183,12 +176,12 @@ ghcModUsage = \ -l\n\ \ Option to be passed to hlint.\n\ \\n\ - \ - root FILE\n\ - \ Try to find the project directory given FILE. For Cabal\n\ - \ projects this is the directory containing the cabal file, for\n\ - \ projects that use a cabal sandbox but have no cabal file this is the\n\ - \ directory containing the sandbox and otherwise this is the directory\n\ - \ containing FILE.\n\ + \ - root\n\ + \ Try to find the project directory. For Cabal projects this is the\n\ + \ directory containing the cabal file, for projects that use a cabal\n\ + \ sandbox but have no cabal file this is the directory containing the\n\ + \ cabal.sandbox.config file and otherwise this is the current\n\ + \ directory.\n\ \\n\ \ - doc MODULE\n\ \ Try finding the html documentation directory for the given MODULE.\n\ @@ -197,57 +190,46 @@ ghcModUsage = \ Print debugging information. Please include the output in any bug\n\ \ reports you submit.\n\ \\n\ - \ - boot\n\ - \ Internal command used by the emacs frontend.\n" - -- "\n\ - -- \The following forms are supported so ghc-mod can be invoked by\n\ - -- \`cabal repl':\n\ - -- \\n\ - -- \ ghc-mod --make GHC_OPTIONS\n\ - -- \ Pass all options through to the GHC executable.\n\ - -- \\n\ - -- \ ghc-mod --interactive GHC_OPTIONS [--ghc-mod]\n\ - -- \ Start ghci emulation mode. GHC_OPTIONS are passed to the\n\ - -- \ GHC API. If `--ghc-mod' is given ghc-mod specific extensions\n\ - -- \ are enabled.\n" - where - indent = (" "++) - -ghcModiUsage :: String -ghcModiUsage = - "Usage: ghc-modi [OPTIONS...] COMMAND\n\ - \*Options*\n" - ++ (unlines $ indent <$> optionUsage indent globalArgSpec) ++ - "*Commands*\n\ - \ - version | --version\n\ - \ Print the version of the program.\n\ + \ - debugComponent [MODULE_OR_FILE...]\n\ + \ Debugging information related to cabal component resolution.\n\ \\n\ - \ - help | --help\n\ - \ Print this help message.\n" + \ - boot\n\ + \ Internal command used by the emacs frontend.\n\ + \\n\ + \ - legacy-interactive\n\ + \ ghc-modi compatibility mode.\n" where indent = (" "++) - - - cmdUsage :: String -> String -> String -cmdUsage cmd s = +cmdUsage cmd realUsage = let -- Find command head - a = dropWhile (not . ((" - " ++ cmd) `isInfixOf`)) $ lines s + a = dropWhile (not . isCmdHead) $ lines realUsage -- Take til the end of the current command block b = flip takeWhile a $ \l -> - all isSpace l || (isIndented l && (isCurrCmdHead l || isNotCmdHead l)) + all isSpace l || (isIndented l && (isCmdHead l || isNotCmdHead l)) -- Drop extra newline from the end c = dropWhileEnd (all isSpace) b isIndented = (" " `isPrefixOf`) isNotCmdHead = ( not . (" - " `isPrefixOf`)) - isCurrCmdHead = ((" - " ++ cmd) `isPrefixOf`) + + containsAnyCmdHead s = ((" - ") `isInfixOf` s) + containsCurrCmdHead s = ((" - " ++ cmd) `isInfixOf` s) + isCmdHead s = + containsAnyCmdHead s && + or [ containsCurrCmdHead s + , any (cmd `isPrefixOf`) (splitOn " | " s) + ] unindent (' ':' ':' ':' ':l) = l unindent l = l in unlines $ unindent <$> c + +ghcModStyle :: Style +ghcModStyle = style { lineLength = 80, ribbonsPerLine = 1.2 } + ---------------------------------------------------------------- option :: [Char] -> [String] -> String -> ArgDescr a -> OptDescr a @@ -256,43 +238,78 @@ option s l udsc dsc = Option s l dsc udsc reqArg :: String -> (String -> a) -> ArgDescr a reqArg udsc dsc = ReqArg dsc udsc -globalArgSpec :: [OptDescr (Options -> Options)] +optArg :: String -> (Maybe String -> a) -> ArgDescr a +optArg udsc dsc = OptArg dsc udsc + +intToLogLevel :: Int -> GmLogLevel +intToLogLevel = toEnum + +globalArgSpec :: [OptDescr (Options -> Either [String] Options)] globalArgSpec = - [ option "v" ["verbose"] "Be more verbose." $ - NoArg $ \o -> o { ghcUserOptions = "-v" : ghcUserOptions o } + [ option "v" ["verbose"] "Increase or set log level. (0-7)" $ + optArg "LEVEL" $ \ml o -> Right $ o { + logLevel = case ml of + Nothing -> increaseLogLevel (logLevel o) + Just l -> toEnum $ min 7 $ read l + } + + , option "s" [] "Be silent, set log level to 0" $ + NoArg $ \o -> Right $ o { logLevel = toEnum 0 } , option "l" ["tolisp"] "Format output as an S-Expression" $ - NoArg $ \o -> o { outputStyle = LispStyle } + NoArg $ \o -> Right $ o { outputStyle = LispStyle } - , option "b" ["boundary"] "Output line separator"$ - reqArg "SEP" $ \s o -> o { lineSeparator = LineSeparator s } + , option "b" ["boundary", "line-seperator"] "Output line separator"$ + reqArg "SEP" $ \s o -> Right $ o { lineSeparator = LineSeparator s } + , option "" ["line-prefix"] "Output line separator"$ + reqArg "OUT,ERR" $ \s o -> let + [out, err] = splitOn "," s + in Right $ o { linePrefix = Just (out, err) } - , option "g" ["ghcOpt"] "Option to be passed to GHC" $ - reqArg "OPT" $ \g o -> + , option "g" ["ghcOpt", "ghc-option"] "Option to be passed to GHC" $ + reqArg "OPT" $ \g o -> Right $ o { ghcUserOptions = g : ghcUserOptions o } , option "" ["with-ghc"] "GHC executable to use" $ - reqArg "PROG" $ \p o -> o { ghcProgram = p } + reqArg "PROG" $ \p o -> Right $ o { ghcProgram = p } + + , option "" ["with-ghc-pkg"] "ghc-pkg executable to use (only needed when guessing from GHC path fails)" $ + reqArg "PROG" $ \p o -> Right $ o { ghcPkgProgram = p } , option "" ["with-cabal"] "cabal-install executable to use" $ - reqArg "PROG" $ \p o -> o { cabalProgram = p } + reqArg "PROG" $ \p o -> Right $ o { cabalProgram = p } + + , option "" ["version"] "print version information" $ + NoArg $ \_ -> Left ["version"] + + , option "" ["help"] "print this help message" $ + NoArg $ \_ -> Left ["help"] + ] parseGlobalArgs :: [String] -> Either InvalidCommandLine (Options, [String]) parseGlobalArgs argv - = case O.getOpt RequireOrder globalArgSpec argv of - (o,r,[] ) -> Right $ (foldr id defaultOptions o, r) - (_,_,errs) -> Left $ InvalidCommandLine $ Right $ - "Parsing command line options failed: " ++ concat errs + = case O.getOpt' RequireOrder globalArgSpec argv of + (o,r,u,[]) -> case foldr (=<<) (Right defaultOptions) o of + Right o' -> Right (o', u ++ r) + Left c -> Right (defaultOptions, c) + (_,_,u,e) -> Left $ InvalidCommandLine $ Right $ + "Parsing command line options failed: " + ++ concat (e ++ map errUnrec u) + where + errUnrec :: String -> String + errUnrec optStr = "unrecognized option `" ++ optStr ++ "'\n" -parseCommandArgs :: [OptDescr (Options -> Options)] +parseCommandArgs :: [OptDescr (Options -> Either [String] Options)] -> [String] -> Options -> (Options, [String]) parseCommandArgs spec argv opts = case O.getOpt RequireOrder (globalArgSpec ++ spec) argv of - (o,r,[]) -> (foldr id opts o, r) + (o,r,[]) -> case foldr (=<<) (Right opts) o of + Right o' -> (o', r) + Left c -> (defaultOptions, c) (_,_,errs) -> fatalError $ "Parsing command options failed: " ++ concat errs @@ -306,121 +323,65 @@ data CmdError = UnknownCommand String instance Exception CmdError ----------------------------------------------------------------- - data InteractiveOptions = InteractiveOptions { ghcModExtensions :: Bool } -instance Default InteractiveOptions where - def = InteractiveOptions False - -handler :: IO a -> IO a -handler = flip catches $ - [ Handler $ \(FatalError msg) -> exitError msg - , Handler $ \(InvalidCommandLine e) -> do +handler :: IOish m => GhcModT m a -> GhcModT m a +handler = flip gcatches $ + [ GHandler $ \(FatalError msg) -> exitError msg + , GHandler $ \(InvalidCommandLine e) -> do case e of Left cmd -> - exitError $ (cmdUsage cmd ghcModUsage) ++ "\n" - ++ progName ++ ": Invalid command line form." - Right msg -> exitError $ progName ++ ": " ++ msg + exitError $ "Usage for `"++cmd++"' command:\n\n" + ++ (cmdUsage cmd usage) ++ "\n" + ++ "ghc-mod: Invalid command line form." + Right msg -> exitError $ "ghc-mod: " ++ msg + , GHandler $ \(SomeException e) -> exitError $ "ghc-mod: " ++ show e ] main :: IO () -main = handler $ do +main = do hSetEncoding stdout utf8 args <- getArgs - - -- This doesn't handle --help and --version being given after any global - -- options. To do that we'd have to fiddle with getOpt. case parseGlobalArgs args of - Left e -> case globalCommands args of - Just s -> putStr s - Nothing -> throw e - - Right res@(_,cmdArgs) -> - case globalCommands cmdArgs of - Just s -> putStr s - Nothing -> progMain res + Left e -> throw e + Right res -> progMain res progMain :: (Options,[String]) -> IO () -progMain (globalOptions,cmdArgs) = do - -- let (ghcArgs, modArgs) = second stripSeperator $ span (/="--") args - -- _realGhcArgs = filter (/="--ghc-mod") ghcArgs - - -- (globalOptions,_cmdArgs) = parseGlobalArgs modArgs - - -- stripSeperator ("--":rest) = rest - -- stripSeperator l = l - - case progName of - "ghc-modi" -> do - legacyInteractive globalOptions =<< emptyNewUnGetLine - - - _ - -- | "--numeric-version" `elem` ghcArgs || "--make" `elem` ghcArgs -> do - -- rawSystem (ghcProgram globalOptions) realGhcArgs >>= exitWith - - -- | "--interactive" `elem` ghcArgs -> do - -- let interactiveOptions = if "--ghc-mod" `elem` ghcArgs - -- then def { ghcModExtensions = True } - -- else def - - -- -- TODO: pass ghcArgs' to ghc API - -- putStrLn "\ninteractive\n" - -- --print realGhcArgs - -- (res, _) <- runGhcModT globalOptions $ undefined - -- case res of - -- Right s -> putStr s - -- Left e -> exitError $ render (gmeDoc e) - - - | otherwise -> do - (res,_) <- runGhcModT globalOptions $ ghcCommands cmdArgs - case res of - Right s -> putStr s - Left e -> exitError $ render (gmeDoc e) - - -- Obtain ghc options by letting ourselfs be executed by - -- @cabal repl@ - -- TODO: need to do something about non-cabal projects - -- exe <- ghcModExecutable - -- let cabalArgs = ["repl", "-v0", "--with-ghc="++exe] - -- ++ (("--ghc-option="++) `map` ("--ghc-mod":"--":args)) - - -- print cabalArgs - - -- rawSystem "cabal" cabalArgs >>= exitWith - +progMain (globalOptions,cmdArgs) = hndle $ runGhcModT globalOptions $ handler $ do + case globalCommands cmdArgs of + Just s -> gmPutStr s + Nothing -> ghcCommands cmdArgs + where + hndle action = do + (e, _l) <- action + case e of + Right _ -> + return () + Left ed -> + exitError' globalOptions $ renderStyle ghcModStyle (gmeDoc ed) +globalCommands :: [String] -> Maybe String +globalCommands (cmd:_) + | cmd == "help" = Just usage + | cmd == "version" = Just ghcModVersion +globalCommands _ = Nothing -- ghc-modi -legacyInteractive :: Options -> UnGetLine -> IO () -legacyInteractive opt ref = flip catches handlers $ do - (res,_) <- runGhcModT opt $ do - symdbreq <- liftIO $ newSymDbReq opt - world <- liftIO . getCurrentWorld =<< cradle - legacyInteractiveLoop symdbreq ref world +legacyInteractive :: IOish m => GhcModT m () +legacyInteractive = do + opt <- options + prepareCabalHelper + tmpdir <- cradleTempDir <$> cradle + symdbreq <- liftIO $ newSymDbReq opt tmpdir + world <- getCurrentWorld + legacyInteractiveLoop symdbreq world - case res of - Right () -> return () - Left e -> putStrLn $ notGood $ render (gmeDoc e) - - where - handlers = [ Handler $ \Restart -> legacyInteractive opt ref ] - -isExitCodeException :: SomeException -> Bool -isExitCodeException e = isJust mExitCode - where - mExitCode :: Maybe ExitCode - mExitCode = fromException e - - -bug :: String -> IO () +bug :: IOish m => String -> GhcModT m () bug msg = do - putStrLn $ notGood $ "BUG: " ++ msg - exitFailure + gmPutStrLn $ notGood $ "BUG: " ++ msg + liftIO exitFailure notGood :: String -> String notGood msg = "NG " ++ escapeNewlines msg @@ -431,30 +392,26 @@ escapeNewlines = replace "\n" "\\n" . replace "\\n" "\\\\n" replace :: String -> String -> String -> String replace needle replacement = intercalate replacement . splitOn needle - legacyInteractiveLoop :: IOish m - => SymDbReq -> UnGetLine -> World -> GhcModT m () -legacyInteractiveLoop symdbreq ref world = do + => SymDbReq -> World -> GhcModT m () +legacyInteractiveLoop symdbreq world = do liftIO . setCurrentDirectory =<< cradleRootDir <$> cradle -- blocking - cmdArg <- liftIO $ getCommand ref + cmdArg <- liftIO $ getLine -- after blocking, we need to see if the world has changed. - changed <- liftIO . didWorldChange world =<< cradle + changed <- didWorldChange world when changed $ do - liftIO $ ungetCommand ref cmdArg - throw Restart - - liftIO . prepareAutogen =<< cradle + dropSession let (cmd':args') = split (keepDelimsR $ condense $ whenElt isSpace) cmdArg arg = concat args' cmd = dropWhileEnd isSpace cmd' args = dropWhileEnd isSpace `map` args' - res <- case dropWhileEnd isSpace cmd of + res <- flip gcatches interactiveHandlers $ case dropWhileEnd isSpace cmd of "check" -> checkSyntaxCmd [arg] "lint" -> lintCmd [arg] "find" -> do @@ -476,22 +433,20 @@ legacyInteractiveLoop symdbreq ref world = do "" -> liftIO $ exitSuccess _ -> fatalError $ "unknown command: `" ++ cmd ++ "'" - liftIO $ putStr res >> putStrLn "OK" >> hFlush stdout - legacyInteractiveLoop symdbreq ref world - - -globalCommands :: [String] -> Maybe String -globalCommands [] = Nothing -globalCommands (cmd:_) = case cmd of - _ | cmd == "help" || cmd == "--help" -> Just usage - _ | cmd == "version" || cmd == "--version" -> Just progVersion - _ -> Nothing - -ghcCommands :: IOish m => [String] -> GhcModT m String -ghcCommands [] = fatalError "No command given (try --help)" -ghcCommands (cmd:args) = fn args + gmPutStr res >> gmPutStrLn "OK" >> liftIO (hFlush stdout) + legacyInteractiveLoop symdbreq world where - fn = case cmd of + interactiveHandlers = + [ GHandler $ \e@(FatalError _) -> throw e + , GHandler $ \(SomeException e) -> gmErrStrLn (show e) >> return "" + ] + +ghcCommands :: IOish m => [String] -> GhcModT m () +ghcCommands [] = fatalError "No command given (try --help)" +ghcCommands (cmd:args) = do + gmPutStr =<< action args + where + action = case cmd of _ | cmd == "list" || cmd == "modules" -> modulesCmd "lang" -> languagesCmd "flag" -> flagsCmd @@ -499,6 +454,7 @@ ghcCommands (cmd:args) = fn args "check" -> checkSyntaxCmd "expand" -> expandTemplateCmd "debug" -> debugInfoCmd + "debug-component" -> componentInfoCmd "info" -> infoCmd "type" -> typesCmd "split" -> splitsCmd @@ -511,6 +467,8 @@ ghcCommands (cmd:args) = fn args "doc" -> pkgDocCmd "dumpsym" -> dumpSymbolCmd "boot" -> bootCmd + "legacy-interactive" -> legacyInteractiveCmd +-- "nuke-caches" -> nukeCachesCmd _ -> fatalError $ "unknown command: `" ++ cmd ++ "'" newtype FatalError = FatalError String deriving (Show, Typeable) @@ -520,14 +478,18 @@ newtype InvalidCommandLine = InvalidCommandLine (Either String String) deriving (Show, Typeable) instance Exception InvalidCommandLine -exitError :: String -> IO a -exitError msg = hPutStrLn stderr msg >> exitFailure +exitError :: IOish m => String -> GhcModT m a +exitError msg = gmErrStrLn (dropWhileEnd (=='\n') msg) >> liftIO exitFailure + +exitError' :: Options -> String -> IO a +exitError' opts msg = + gmUnsafeErrStrLn opts (dropWhileEnd (=='\n') msg) >> liftIO exitFailure fatalError :: String -> a -fatalError s = throw $ FatalError $ progName ++ ": " ++ s +fatalError s = throw $ FatalError $ "ghc-mod: " ++ s withParseCmd :: IOish m - => [OptDescr (Options -> Options)] + => [OptDescr (Options -> Either [String] Options)] -> ([String] -> GhcModT m a) -> [String] -> GhcModT m a @@ -535,25 +497,43 @@ withParseCmd spec action args = do (opts', rest) <- parseCommandArgs spec args <$> options withOptions (const opts') $ action rest +withParseCmd' :: (IOish m, ExceptionMonad m) + => String + -> [OptDescr (Options -> Either [String] Options)] + -> ([String] -> GhcModT m a) + -> [String] + -> GhcModT m a +withParseCmd' cmd spec action args = + catchArgs cmd $ withParseCmd spec action args + +catchArgs :: (Monad m, ExceptionMonad m) => String -> m a -> m a +catchArgs cmd action = + action `gcatch` \(PatternMatchFail _) -> + throw $ InvalidCommandLine (Left cmd) + modulesCmd, languagesCmd, flagsCmd, browseCmd, checkSyntaxCmd, expandTemplateCmd, - debugInfoCmd, infoCmd, typesCmd, splitsCmd, sigCmd, refineCmd, autoCmd, - findSymbolCmd, lintCmd, rootInfoCmd, pkgDocCmd, dumpSymbolCmd, bootCmd + debugInfoCmd, componentInfoCmd, infoCmd, typesCmd, splitsCmd, sigCmd, + refineCmd, autoCmd, findSymbolCmd, lintCmd, rootInfoCmd, pkgDocCmd, + dumpSymbolCmd, bootCmd, legacyInteractiveCmd, nukeCachesCmd :: IOish m => [String] -> GhcModT m String -modulesCmd = withParseCmd [] $ \[] -> modules -languagesCmd = withParseCmd [] $ \[] -> languages -flagsCmd = withParseCmd [] $ \[] -> flags -debugInfoCmd = withParseCmd [] $ \[] -> debugInfo -rootInfoCmd = withParseCmd [] $ \[] -> rootInfo +modulesCmd = withParseCmd' "modules" s $ \[] -> modules + where s = modulesArgSpec +languagesCmd = withParseCmd' "lang" [] $ \[] -> languages +flagsCmd = withParseCmd' "flag" [] $ \[] -> flags +debugInfoCmd = withParseCmd' "debug" [] $ \[] -> debugInfo +rootInfoCmd = withParseCmd' "root" [] $ \[] -> rootInfo +componentInfoCmd = withParseCmd' "debugComponent" [] $ \ts -> componentInfo ts -- internal -bootCmd = withParseCmd [] $ \[] -> boot +bootCmd = withParseCmd' "boot" [] $ \[] -> boot +nukeCachesCmd = withParseCmd' "nuke-caches" [] $ \[] -> nukeCaches >> return "" -dumpSymbolCmd = withParseCmd [] $ \[tmpdir] -> dumpSymbol tmpdir -findSymbolCmd = withParseCmd [] $ \[sym] -> findSymbol sym -pkgDocCmd = withParseCmd [] $ \[mdl] -> pkgDoc mdl -lintCmd = withParseCmd s $ \[file] -> lint file +dumpSymbolCmd = withParseCmd' "dump" [] $ \[tmpdir] -> dumpSymbol tmpdir +findSymbolCmd = withParseCmd' "find" [] $ \[sym] -> findSymbol sym +pkgDocCmd = withParseCmd' "doc" [] $ \[mdl] -> pkgDoc mdl +lintCmd = withParseCmd' "lint" s $ \[file] -> lint file where s = hlintArgSpec -browseCmd = withParseCmd s $ \mdls -> concat <$> browse `mapM` mdls +browseCmd = withParseCmd s $ \mdls -> concat <$> browse `mapM` mdls where s = browseArgSpec checkSyntaxCmd = withParseCmd [] $ checkAction checkSyntax expandTemplateCmd = withParseCmd [] $ checkAction expandTemplate @@ -565,10 +545,20 @@ autoCmd = withParseCmd [] $ locAction "auto" auto refineCmd = withParseCmd [] $ locAction' "refine" refine infoCmd = withParseCmd [] $ action - where action [file,_,expr] = info file expr - action [file,expr] = info file expr + where action [file,_,expr] = info file $ Expression expr + action [file,expr] = info file $ Expression expr action _ = throw $ InvalidCommandLine (Left "info") +legacyInteractiveCmd = withParseCmd [] go + where + go [] = + legacyInteractive >> return "" + go ("help":[]) = + return usage + go ("version":[]) = + return ghcModiVersion + go _ = throw $ InvalidCommandLine (Left "legacy-interactive") + checkAction :: ([t] -> a) -> [t] -> a checkAction _ [] = throw $ InvalidCommandLine (Right "No files given.") checkAction action files = action files @@ -578,22 +568,43 @@ locAction _ action [file,_,line,col] = action file (read line) (read col) locAction _ action [file, line,col] = action file (read line) (read col) locAction cmd _ _ = throw $ InvalidCommandLine (Left cmd) -locAction' :: String -> (String -> Int -> Int -> String -> a) -> [String] -> a -locAction' _ action [f,_,line,col,expr] = action f (read line) (read col) expr -locAction' _ action [f, line,col,expr] = action f (read line) (read col) expr +locAction' :: String -> (String -> Int -> Int -> Expression -> a) -> [String] -> a +locAction' _ action [f,_,line,col,expr] = action f (read line) (read col) (Expression expr) +locAction' _ action [f, line,col,expr] = action f (read line) (read col) (Expression expr) locAction' cmd _ _ = throw $ InvalidCommandLine (Left cmd) -hlintArgSpec :: [OptDescr (Options -> Options)] + +modulesArgSpec :: [OptDescr (Options -> Either [String] Options)] +modulesArgSpec = + [ option "d" ["detailed"] "Print package modules belong to." $ + NoArg $ \o -> Right $ o { detailed = True } + ] + + +hlintArgSpec :: [OptDescr (Options -> Either [String] Options)] hlintArgSpec = [ option "h" ["hlintOpt"] "Option to be passed to hlint" $ - reqArg "hlintOpt" $ \h o -> o { hlintOpts = h : hlintOpts o } + reqArg "hlintOpt" $ \h o -> Right $ o { hlintOpts = h : hlintOpts o } ] -browseArgSpec :: [OptDescr (Options -> Options)] + +browseArgSpec :: [OptDescr (Options -> Either [String] Options)] browseArgSpec = [ option "o" ["operators"] "Also print operators." $ - NoArg $ \o -> o { operators = True } + NoArg $ \o -> Right $ o { operators = True } , option "d" ["detailed"] "Print symbols with accompanying signature." $ - NoArg $ \o -> o { detailed = True } + NoArg $ \o -> Right $ o { detailed = True } , option "q" ["qualified"] "Qualify symbols" $ - NoArg $ \o -> o { qualified = True } + NoArg $ \o -> Right $ o { qualified = True } ] + +nukeCaches :: IOish m => GhcModT m () +nukeCaches = do + chdir <- liftIO $ ( "cabal-helper") <$> getAppUserDataDirectory "ghc-mod" + c <- cradle + + when (cradleProjectType c == CabalProject) $ do + let root = cradleRootDir c + liftIO $ (trySome . removeDirectoryRecursive) `mapM_` [chdir, root "dist"] + +trySome :: IO a -> IO (Either SomeException a) +trySome = try diff --git a/src/GHCModi.hs b/src/GHCModi.hs index c9e958c..161bc76 100644 --- a/src/GHCModi.hs +++ b/src/GHCModi.hs @@ -1,262 +1,55 @@ {-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-} -- | WARNING --- This program in the process of being deprecated, use `ghc-mod --interactive` --- instead. - --- Commands: --- check --- find --- info --- type --- lint [hlint options] --- the format of hlint options is [String] because they may contain --- spaces and also may contain spaces. --- boot --- browse [:] --- quit --- --- Session separators: --- OK -- success --- NG -- failure +-- This program is deprecated, use `ghc-mod legacy-interactive` instead. module Main where -import Config (cProjectVersion) -import Control.Applicative ((<$>)) -import Control.Exception (SomeException(..)) -import qualified Control.Exception as E -import Control.Monad (when) -import CoreMonad (liftIO) -import Data.List (intercalate) -import Data.List.Split (splitOn) -import Data.Version (showVersion) -import Language.Haskell.GhcMod -import Language.Haskell.GhcMod.Internal +import Control.Applicative +import Control.Monad +import Control.Exception +import Data.Version +import Data.Maybe +import System.IO +import System.Exit +import System.Process +import System.FilePath +import System.Environment import Paths_ghc_mod -import System.Console.GetOpt -import System.Directory (setCurrentDirectory) -import System.Environment (getArgs) -import System.Exit (ExitCode, exitFailure) -import System.IO (hFlush,stdout) - -import Misc import Utils - ----------------------------------------------------------------- - -progVersion :: String -progVersion = "ghc-modi version " ++ showVersion version ++ " compiled by GHC " ++ cProjectVersion ++ "\n" - -argspec :: [OptDescr (Options -> Options)] -argspec = [ Option "b" ["boundary"] - (ReqArg (\s opts -> opts { lineSeparator = LineSeparator s }) "sep") - "specify line separator (default is Nul string)" - , Option "l" ["tolisp"] - (NoArg (\opts -> opts { outputStyle = LispStyle })) - "print as a list of Lisp" - , Option "g" [] - (ReqArg (\s opts -> opts { ghcUserOptions = s : ghcUserOptions opts }) "flag") "specify a ghc flag" - ] - -usage :: String -usage = progVersion - ++ "Usage:\n" - ++ "\t ghc-modi [-l] [-b sep] [-g flag]\n" - ++ "\t ghc-modi version\n" - ++ "\t ghc-modi help\n" - -parseArgs :: [OptDescr (Options -> Options)] -> [String] -> (Options, [String]) -parseArgs spec argv - = case getOpt Permute spec argv of - (o,n,[] ) -> (foldr id defaultOptions o, n) - (_,_,errs) -> E.throw (CmdArg errs) - ----------------------------------------------------------------- - --- Running two GHC monad threads disables the handling of --- C-c since installSignalHandlers is called twice, sigh. +import Prelude main :: IO () -main = E.handle cmdHandler $ - go =<< parseArgs argspec <$> getArgs - where - cmdHandler (CmdArg _) = putStr $ usageInfo usage argspec - go (_,"help":_) = putStr $ usageInfo usage argspec - go (_,"version":_) = putStr progVersion - go (opt,_) = emptyNewUnGetLine >>= run opt +main = do + hPutStrLn stderr $ + "Warning: ghc-modi is deprecated please use 'ghc-mod legacy-interactive' instead" -run :: Options -> UnGetLine -> IO () -run opt ref = flip E.catches handlers $ do - cradle0 <- findCradle - let rootdir = cradleRootDir cradle0 --- c = cradle0 { cradleCurrentDir = rootdir } TODO: ????? - setCurrentDirectory rootdir - prepareAutogen cradle0 - -- Asynchronous db loading starts here. - symdbreq <- newSymDbReq opt - (res, _) <- runGhcModT opt $ do - crdl <- cradle - world <- liftIO $ getCurrentWorld crdl - loop symdbreq ref world - case res of - Right () -> return () - Left (GMECabalConfigure msg) -> do - putStrLn $ notGood $ "cabal configure failed: " ++ show msg - exitFailure - Left e -> bug $ show e - where - -- this is just in case. - -- If an error is caught here, it is a bug of GhcMod library. - handlers = [ E.Handler (\(_ :: ExitCode) -> return ()) - , E.Handler (\(_ :: Restart) -> run opt ref) - , E.Handler (\(SomeException e) -> bug $ show e) ] + args <- getArgs + bindir <- getBinDir + let installedExe = bindir "ghc-mod" + mexe <- mplus <$> mightExist installedExe <*> pathExe + case mexe of + Nothing -> do + hPutStrLn stderr $ + "ghc-modi: Could not find '"++installedExe++"', check your installation!" + exitWith $ ExitFailure 1 -bug :: String -> IO () -bug msg = do - putStrLn $ notGood $ "BUG: " ++ msg - exitFailure + Just exe -> do + (_, _, _, h) <- + createProcess $ proc exe $ ["legacy-interactive"] ++ args + exitWith =<< waitForProcess h -notGood :: String -> String -notGood msg = "NG " ++ escapeNewlines msg +pathExe :: IO (Maybe String) +pathExe = do + ev <- try $ words <$> readProcess "ghc-mod" ["--version"] "" + let mexe = case ev of + Left (SomeException _) -> Nothing + Right ["ghc-mod", "version", ver + , "compiled", "by", "GHC", _] + | showVersion version == ver -> do + Just "ghc-mod" + Right _ -> Nothing -escapeNewlines :: String -> String -escapeNewlines = replace "\n" "\\n" . replace "\\n" "\\\\n" - -replace :: String -> String -> String -> String -replace needle replacement = intercalate replacement . splitOn needle - ----------------------------------------------------------------- - -loop :: IOish m => SymDbReq -> UnGetLine -> World -> GhcModT m () -loop symdbreq ref world = do - -- blocking - cmdArg <- liftIO $ getCommand ref - -- after blocking, we need to see if the world has changed. - crdl <- cradle - changed <- liftIO $ didWorldChange world crdl - when changed $ do - liftIO $ ungetCommand ref cmdArg - E.throw Restart - cradle >>= liftIO . prepareAutogen - let (cmd,arg') = break (== ' ') cmdArg - arg = dropWhile (== ' ') arg' - (ret,ok) <- case cmd of - "check" -> checkStx arg - "find" -> findSym arg symdbreq - "lint" -> lintStx arg - "info" -> showInfo arg - "type" -> showType arg - "split" -> doSplit arg - "sig" -> doSig arg - "refine" -> doRefine arg - "auto" -> doAuto arg - "boot" -> bootIt - "browse" -> browseIt arg - "quit" -> return ("quit", False) - "" -> return ("quit", False) - _ -> return ([], True) - if ok then do - liftIO $ putStr ret - liftIO $ putStrLn "OK" - else do - liftIO $ putStrLn $ notGood ret - liftIO $ hFlush stdout - when ok $ loop symdbreq ref world - ----------------------------------------------------------------- - -checkStx :: IOish m => FilePath -> GhcModT m (String, Bool) -checkStx file = do - eret <- check [file] - case eret of - Right ret -> return (ret, True) - Left ret -> return (ret, True) - ----------------------------------------------------------------- - -findSym :: IOish m => Symbol -> SymDbReq -> GhcModT m (String, Bool) -findSym sym symdbreq = do - db <- getDb symdbreq >>= checkDb symdbreq - ret <- lookupSymbol sym db - return (ret, True) - -lintStx :: IOish m => FilePath -> GhcModT m (String, Bool) -lintStx optFile = do - ret <- withOptions changeOpt $ lint file - return (ret, True) - where - (opts,file) = parseLintOptions optFile - hopts = if opts == "" then [] else read opts - changeOpt o = o { hlintOpts = hopts } - --- | --- >>> parseLintOptions "[\"--ignore=Use camelCase\", \"--ignore=Eta reduce\"] file name" --- (["--ignore=Use camelCase", "--ignore=Eta reduce"], "file name") --- >>> parseLintOptions "file name" --- ([], "file name") -parseLintOptions :: String -> (String, String) -parseLintOptions optFile = case brk (== ']') (dropWhile (/= '[') optFile) of - ("","") -> ([], optFile) - (opt',file') -> (opt', dropWhile (== ' ') file') - where - brk _ [] = ([],[]) - brk p (x:xs') - | p x = ([x],xs') - | otherwise = let (ys,zs) = brk p xs' in (x:ys,zs) - ----------------------------------------------------------------- - -showInfo :: IOish m => FilePath -> GhcModT m (String, Bool) -showInfo fileArg = do - let [file, expr] = splitN 2 fileArg - ret <- info file expr - return (ret, True) - -showType :: IOish m => FilePath -> GhcModT m (String, Bool) -showType fileArg = do - let [file, line, column] = splitN 3 fileArg - ret <- types file (read line) (read column) - return (ret, True) - -doSplit :: IOish m => FilePath -> GhcModT m (String, Bool) -doSplit fileArg = do - let [file, line, column] = splitN 3 fileArg - ret <- splits file (read line) (read column) - return (ret, True) - -doSig :: IOish m => FilePath -> GhcModT m (String, Bool) -doSig fileArg = do - let [file, line, column] = splitN 3 fileArg - ret <- sig file (read line) (read column) - return (ret, True) - -doRefine :: IOish m => FilePath -> GhcModT m (String, Bool) -doRefine fileArg = do - let [file, line, column, expr] = splitN 4 fileArg - ret <- refine file (read line) (read column) expr - return (ret, True) - -doAuto :: IOish m => FilePath -> GhcModT m (String, Bool) -doAuto fileArg = do - let [file, line, column] = splitN 3 fileArg - ret <- auto file (read line) (read column) - return (ret, True) - ----------------------------------------------------------------- - -bootIt :: IOish m => GhcModT m (String, Bool) -bootIt = do - ret <- boot - return (ret, True) - -browseIt :: IOish m => ModuleString -> GhcModT m (String, Bool) -browseIt mdl = do - let (det,rest') = break (== ' ') mdl - rest = dropWhile (== ' ') rest' - ret <- if det == "-d" - then withOptions setDetailed (browse rest) - else browse mdl - return (ret, True) - where - setDetailed opt = opt { detailed = True } + when (isNothing mexe) $ + hPutStrLn stderr "ghc-modi: ghc-mod executable on PATH has different version, check your installation!" + return mexe diff --git a/src/Misc.hs b/src/Misc.hs index 21248ad..bc5ff9d 100644 --- a/src/Misc.hs +++ b/src/Misc.hs @@ -1,75 +1,28 @@ {-# LANGUAGE DeriveDataTypeable, CPP #-} module Misc ( - GHCModiError(..) - , Restart(..) - , UnGetLine - , emptyNewUnGetLine - , ungetCommand - , getCommand - , SymDbReq + SymDbReq , newSymDbReq , getDb , checkDb - , prepareAutogen ) where -import Control.Applicative ((<$>)) -import Control.Concurrent (threadDelay) import Control.Concurrent.Async (Async, async, wait) -import Control.Exception (Exception) -import Control.Monad (unless, when) import CoreMonad (liftIO) import Data.IORef (IORef, newIORef, readIORef, writeIORef) -import Data.List (isPrefixOf) -import Data.Maybe (isJust) -import Data.Typeable (Typeable) -import System.Directory (doesDirectoryExist, getDirectoryContents) -import System.IO (openBinaryFile, IOMode(..)) -import System.Process +import Prelude import Language.Haskell.GhcMod -import Language.Haskell.GhcMod.Internal - ----------------------------------------------------------------- - -data GHCModiError = CmdArg [String] deriving (Show, Typeable) - -instance Exception GHCModiError - ----------------------------------------------------------------- - -data Restart = Restart deriving (Show, Typeable) - -instance Exception Restart - ----------------------------------------------------------------- - -newtype UnGetLine = UnGetLine (IORef (Maybe String)) - -emptyNewUnGetLine :: IO UnGetLine -emptyNewUnGetLine = UnGetLine <$> newIORef Nothing - -ungetCommand :: UnGetLine -> String -> IO () -ungetCommand (UnGetLine ref) cmd = writeIORef ref (Just cmd) - -getCommand :: UnGetLine -> IO String -getCommand (UnGetLine ref) = do - mcmd <- readIORef ref - case mcmd of - Nothing -> getLine - Just cmd -> do - writeIORef ref Nothing - return cmd +import Language.Haskell.GhcMod.Internal hiding (MonadIO,liftIO) ---------------------------------------------------------------- type SymDbReqAction = (Either GhcModError SymbolDb, GhcModLog) data SymDbReq = SymDbReq (IORef (Async SymDbReqAction)) (IO SymDbReqAction) -newSymDbReq :: Options -> IO SymDbReq -newSymDbReq opt = do - let act = runGhcModT opt loadSymbolDb +newSymDbReq :: Options -> FilePath -> IO SymDbReq +newSymDbReq opt dir = do + let act = runGhcModT opt $ loadSymbolDb dir req <- async act ref <- newIORef req return $ SymDbReq ref act @@ -83,7 +36,7 @@ getDb (SymDbReq ref _) = do checkDb :: IOish m => SymDbReq -> SymbolDb -> GhcModT m SymbolDb checkDb (SymDbReq ref act) db = do - outdated <- liftIO $ isOutdated db + outdated <- isOutdated db if outdated then do -- async and wait here is unnecessary because this is essentially -- synchronous. But Async can be used a cache. @@ -92,63 +45,3 @@ checkDb (SymDbReq ref act) db = do hoistGhcModT =<< liftIO (wait req) else return db - ----------------------------------------------------------------- - -build :: IO ProcessHandle -build = do -#ifdef WINDOWS - nul <- openBinaryFile "NUL" AppendMode -#else - nul <- openBinaryFile "/dev/null" AppendMode -#endif - (_, _, _, hdl) <- createProcess $ pro nul - return hdl - where - pro nul = CreateProcess { - cmdspec = RawCommand "cabal" ["build"] - , cwd = Nothing - , env = Nothing - , std_in = Inherit - , std_out = UseHandle nul - , std_err = UseHandle nul - , close_fds = False -#if __GLASGOW_HASKELL__ >= 702 - , create_group = True -#endif -#if __GLASGOW_HASKELL__ >= 707 - , delegate_ctlc = False -#endif - } - -autogen :: String -autogen = "dist/build/autogen" - -isAutogenPrepared :: IO Bool -isAutogenPrepared = do - exist <- doesDirectoryExist autogen - if exist then do - files <- filter ("." `isPrefixOf`) <$> getDirectoryContents autogen - if length files >= 2 then - return True - else - return False - else - return False - -watch :: Int -> ProcessHandle -> IO () -watch 0 _ = return () -watch n hdl = do - prepared <- isAutogenPrepared - if prepared then - interruptProcessGroupOf hdl - else do - threadDelay 100000 - watch (n - 1) hdl - -prepareAutogen :: Cradle -> IO () -prepareAutogen crdl = when (isJust $ cradleCabalFile crdl) $ do - prepared <- isAutogenPrepared - unless prepared $ do - hdl <- build - watch 30 hdl diff --git a/src/Utils.hs b/src/Utils.hs deleted file mode 100644 index c91151d..0000000 --- a/src/Utils.hs +++ /dev/null @@ -1,27 +0,0 @@ -module Utils where - --- | --- --- >>> split "foo bar baz" --- ["foo","bar baz"] --- >>> split "foo bar baz" --- ["foo","bar baz"] -split :: String -> [String] -split xs = [ys, dropWhile isSpace zs] - where - isSpace = (== ' ') - (ys,zs) = break isSpace xs - --- | --- --- >>> splitN 0 "foo bar baz" --- ["foo","bar baz"] --- >>> splitN 2 "foo bar baz" --- ["foo","bar baz"] --- >>> splitN 3 "foo bar baz" --- ["foo","bar","baz"] -splitN :: Int -> String -> [String] -splitN n xs - | n <= 2 = split xs - | otherwise = let [ys,zs] = split xs - in ys : splitN (n - 1) zs diff --git a/test/BrowseSpec.hs b/test/BrowseSpec.hs index b56f286..d615d50 100644 --- a/test/BrowseSpec.hs +++ b/test/BrowseSpec.hs @@ -26,7 +26,8 @@ spec = do syms `shouldContain` ["Left :: a -> Either a b"] describe "`browse' in a project directory" $ do - it "lists symbols defined in a a local module (e.g. `Baz.baz)" $ do - withDirectory_ "test/data" $ do - syms <- runID $ lines <$> browse "Baz" - syms `shouldContain` ["baz"] + it "can list symbols defined in a a local module" $ do + withDirectory_ "test/data/ghc-mod-check/lib" $ do + syms <- runD $ lines <$> browse "Data.Foo" + syms `shouldContain` ["foo"] + syms `shouldContain` ["fibonacci"] diff --git a/test/CabalApiSpec.hs b/test/CabalApiSpec.hs deleted file mode 100644 index 67aa9f4..0000000 --- a/test/CabalApiSpec.hs +++ /dev/null @@ -1,77 +0,0 @@ -{-# LANGUAGE ScopedTypeVariables #-} - -module CabalApiSpec where - -import Control.Applicative -import Language.Haskell.GhcMod.CabalApi -import Language.Haskell.GhcMod.Cradle -import Language.Haskell.GhcMod.Types -import Test.Hspec -import System.Directory -import System.FilePath -import System.Process (readProcess) - -import Dir -import TestUtils - -import Config (cProjectVersionInt) -- ghc version - -ghcVersion :: Int -ghcVersion = read cProjectVersionInt - -spec :: Spec -spec = do - describe "parseCabalFile" $ do - it "throws an exception if the cabal file is broken" $ do - shouldReturnError $ do - withDirectory_ "test/data/broken-cabal" $ do - crdl <- findCradle - runD' $ parseCabalFile crdl "broken.cabal" - - - describe "getCompilerOptions" $ do - it "gets necessary CompilerOptions" $ do - cwd <- getCurrentDirectory - withDirectory "test/data/subdir1/subdir2" $ \dir -> do - crdl <- findCradle - let Just cabalFile = cradleCabalFile crdl - pkgDesc <- runD $ parseCabalFile crdl cabalFile - res <- runD $ getCompilerOptions [] crdl pkgDesc - let res' = res { - ghcOptions = ghcOptions res - , includeDirs = map (toRelativeDir dir) (includeDirs res) - } - if ghcVersion < 706 - then ghcOptions res' `shouldContain` ["-global-package-conf", "-no-user-package-conf","-package-conf",cwd "test/data/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d","-XHaskell98"] - else ghcOptions res' `shouldContain` ["-global-package-db", "-no-user-package-db","-package-db",cwd "test/data/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d","-XHaskell98"] - includeDirs res' `shouldBe` ["test/data","test/data/dist/build","test/data/dist/build/autogen","test/data/subdir1/subdir2","test/data/test"] - (pkgName `map` depPackages res') `shouldContain` ["Cabal"] - - - describe "cabalDependPackages" $ do - it "extracts dependent packages" $ do - crdl <- findCradle' "test/data/" - pkgs <- cabalDependPackages . cabalAllBuildInfo <$> runD (parseCabalFile crdl "test/data/cabalapi.cabal") - pkgs `shouldBe` ["Cabal","base","template-haskell"] - it "uses non default flags" $ do - withDirectory_ "test/data/cabal-flags" $ do - crdl <- findCradle - _ <- readProcess "cabal" ["configure", "-ftest-flag"] "" - pkgs <- cabalDependPackages . cabalAllBuildInfo <$> runD (parseCabalFile crdl "cabal-flags.cabal") - pkgs `shouldBe` ["Cabal","base"] - - describe "cabalSourceDirs" $ do - it "extracts all hs-source-dirs" $ do - crdl <- findCradle' "test/data/check-test-subdir" - dirs <- cabalSourceDirs . cabalAllBuildInfo <$> runD (parseCabalFile crdl "test/data/check-test-subdir/check-test-subdir.cabal") - dirs `shouldBe` ["src", "test"] - it "extracts all hs-source-dirs including \".\"" $ do - crdl <- findCradle' "test/data/" - dirs <- cabalSourceDirs . cabalAllBuildInfo <$> runD (parseCabalFile crdl "test/data/cabalapi.cabal") - dirs `shouldBe` [".", "test"] - - describe "cabalAllBuildInfo" $ do - it "extracts build info" $ do - crdl <- findCradle' "test/data/" - info <- cabalAllBuildInfo <$> runD (parseCabalFile crdl "test/data/cabalapi.cabal") - show info `shouldBe` "[BuildInfo {buildable = True, buildTools = [], cppOptions = [], ccOptions = [], ldOptions = [], pkgconfigDepends = [], frameworks = [], cSources = [], hsSourceDirs = [\".\"], otherModules = [ModuleName [\"Browse\"],ModuleName [\"CabalApi\"],ModuleName [\"Cabal\"],ModuleName [\"CabalDev\"],ModuleName [\"Check\"],ModuleName [\"ErrMsg\"],ModuleName [\"Flag\"],ModuleName [\"GHCApi\"],ModuleName [\"GHCChoice\"],ModuleName [\"Gap\"],ModuleName [\"Info\"],ModuleName [\"Lang\"],ModuleName [\"Lint\"],ModuleName [\"List\"],ModuleName [\"Paths_ghc_mod\"],ModuleName [\"Types\"]], defaultLanguage = Nothing, otherLanguages = [], defaultExtensions = [], otherExtensions = [], oldExtensions = [], extraLibs = [], extraLibDirs = [], includeDirs = [], includes = [], installIncludes = [], options = [(GHC,[\"-Wall\"])], ghcProfOptions = [], ghcSharedOptions = [], customFieldsBI = [], targetBuildDepends = [Dependency (PackageName \"Cabal\") (UnionVersionRanges (ThisVersion (Version {versionBranch = [1,10], versionTags = []})) (LaterVersion (Version {versionBranch = [1,10], versionTags = []}))),Dependency (PackageName \"base\") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4,0], versionTags = []})) (LaterVersion (Version {versionBranch = [4,0], versionTags = []}))) (EarlierVersion (Version {versionBranch = [5], versionTags = []}))),Dependency (PackageName \"template-haskell\") AnyVersion]},BuildInfo {buildable = True, buildTools = [], cppOptions = [], ccOptions = [], ldOptions = [], pkgconfigDepends = [], frameworks = [], cSources = [], hsSourceDirs = [\"test\",\".\"], otherModules = [ModuleName [\"Expectation\"],ModuleName [\"BrowseSpec\"],ModuleName [\"CabalApiSpec\"],ModuleName [\"FlagSpec\"],ModuleName [\"LangSpec\"],ModuleName [\"LintSpec\"],ModuleName [\"ListSpec\"]], defaultLanguage = Nothing, otherLanguages = [], defaultExtensions = [], otherExtensions = [], oldExtensions = [], extraLibs = [], extraLibDirs = [], includeDirs = [], includes = [], installIncludes = [], options = [], ghcProfOptions = [], ghcSharedOptions = [], customFieldsBI = [], targetBuildDepends = [Dependency (PackageName \"Cabal\") (UnionVersionRanges (ThisVersion (Version {versionBranch = [1,10], versionTags = []})) (LaterVersion (Version {versionBranch = [1,10], versionTags = []}))),Dependency (PackageName \"base\") (IntersectVersionRanges (UnionVersionRanges (ThisVersion (Version {versionBranch = [4,0], versionTags = []})) (LaterVersion (Version {versionBranch = [4,0], versionTags = []}))) (EarlierVersion (Version {versionBranch = [5], versionTags = []})))]}]" diff --git a/test/CabalHelperSpec.hs b/test/CabalHelperSpec.hs new file mode 100644 index 0000000..42211d8 --- /dev/null +++ b/test/CabalHelperSpec.hs @@ -0,0 +1,96 @@ +module CabalHelperSpec where + +import Control.Arrow +import Control.Applicative +import Distribution.Helper +import Language.Haskell.GhcMod.CabalHelper +import Language.Haskell.GhcMod.PathsAndFiles +import Language.Haskell.GhcMod.Error +import Test.Hspec +import System.Directory +import System.FilePath +import System.Process (readProcess, system) + +import Dir +import TestUtils +import Data.List + +import Config (cProjectVersionInt) + +ghcVersion :: Int +ghcVersion = read cProjectVersionInt + +gmeProcessException :: GhcModError -> Bool +gmeProcessException GMEProcess {} = True +gmeProcessException _ = False + +pkgOptions :: [String] -> [String] +pkgOptions [] = [] +pkgOptions (_:[]) = [] +pkgOptions (x:y:xs) | x == "-package-id" = [name y] ++ pkgOptions xs + | otherwise = pkgOptions (y:xs) + where + stripDash s = maybe s id $ (flip drop s . (+1) <$> findIndex (=='-') s) + name s = reverse $ stripDash $ stripDash $ reverse s + +idirOpts :: [(c, [String])] -> [(c, [String])] +idirOpts = map (second $ map (drop 2) . filter ("-i"`isPrefixOf`)) + +spec :: Spec +spec = do + describe "getComponents" $ do + it "throws an exception if the cabal file is broken" $ do + let tdir = "test/data/broken-cabal" + runD' tdir getComponents `shouldThrow` anyIOException + + it "handles sandboxes correctly" $ do + let tdir = "test/data/cabal-project" + cwd <- getCurrentDirectory + + -- TODO: ChSetupHsName should also have sandbox stuff, see related + -- comment in cabal-helper + opts <- map gmcGhcOpts . filter ((/= ChSetupHsName) . gmcName) <$> runD' tdir getComponents + + bp <- buildPlatform readProcess + if ghcVersion < 706 + then forM_ opts (\o -> o `shouldContain` ["-no-user-package-conf","-package-conf", cwd "test/data/cabal-project/.cabal-sandbox/"++ghcSandboxPkgDbDir bp]) + else forM_ opts (\o -> o `shouldContain` ["-no-user-package-db","-package-db",cwd "test/data/cabal-project/.cabal-sandbox/"++ghcSandboxPkgDbDir bp]) + + it "extracts build dependencies" $ do + let tdir = "test/data/cabal-project" + opts <- map gmcGhcOpts <$> runD' tdir getComponents + let ghcOpts = head opts + pkgs = pkgOptions ghcOpts + pkgs `shouldBe` ["Cabal","base","template-haskell"] + + it "uses non default flags" $ do + let tdir = "test/data/cabal-flags" + _ <- withDirectory_ tdir $ + readProcess "cabal" ["configure", "-ftest-flag"] "" + + opts <- map gmcGhcOpts <$> runD' tdir getComponents + let ghcOpts = head opts + pkgs = pkgOptions ghcOpts + pkgs `shouldBe` ["Cabal","base"] + + describe "getCustomPkgDbStack" $ do + it "works" $ do + let tdir = "test/data/custom-cradle" + Just stack <- runD' tdir $ getCustomPkgDbStack + stack `shouldBe` [ GlobalDb + , UserDb + , PackageDb "package-db-a" + , PackageDb "package-db-b" + , PackageDb "package-db-c" + ] + + describe "getPackageDbStack'" $ do + it "fixes out of sync custom pkg-db stack" $ do + withDirectory_ "test/data/custom-cradle" $ do + _ <- system "cabal configure" + (s, s') <- runD $ do + Just stack <- getCustomPkgDbStack + withCabal $ do + stack' <- getCabalPackageDbStack + return (stack, stack') + s' `shouldBe` s diff --git a/test/CheckSpec.hs b/test/CheckSpec.hs index 35e5992..cc9b219 100644 --- a/test/CheckSpec.hs +++ b/test/CheckSpec.hs @@ -1,9 +1,10 @@ {-# LANGUAGE CPP #-} module CheckSpec where -import Data.List (isSuffixOf, isInfixOf, isPrefixOf) import Language.Haskell.GhcMod -import System.FilePath + +import Data.List +import System.Process import Test.Hspec import TestUtils @@ -14,38 +15,55 @@ spec = do describe "checkSyntax" $ do it "works even if an executable depends on the library defined in the same cabal file" $ do withDirectory_ "test/data/ghc-mod-check" $ do - res <- runID $ checkSyntax ["main.hs"] + res <- runD $ checkSyntax ["main.hs"] res `shouldBe` "main.hs:5:1:Warning: Top-level binding with no type signature: main :: IO ()\n" + it "works even if a module imports another module from a different directory" $ do withDirectory_ "test/data/check-test-subdir" $ do - res <- runID $ checkSyntax ["test/Bar/Baz.hs"] + _ <- system "cabal configure --enable-tests" + res <- runD $ checkSyntax ["test/Bar/Baz.hs"] res `shouldSatisfy` (("test" "Foo.hs:3:1:Warning: Top-level binding with no type signature: foo :: [Char]\n") `isSuffixOf`) it "detects cyclic imports" $ do - withDirectory_ "test/data" $ do - res <- runID $ checkSyntax ["Mutual1.hs"] + withDirectory_ "test/data/import-cycle" $ do + res <- runD $ checkSyntax ["Mutual1.hs"] res `shouldSatisfy` ("Module imports form a cycle" `isInfixOf`) it "works with modules using QuasiQuotes" $ do - withDirectory_ "test/data" $ do - res <- runID $ checkSyntax ["Baz.hs"] - res `shouldSatisfy` ("Baz.hs:5:1:Warning:" `isPrefixOf`) + withDirectory_ "test/data/quasi-quotes" $ do + res <- runD $ checkSyntax ["QuasiQuotes.hs"] + res `shouldSatisfy` ("QuasiQuotes.hs:6:1:Warning:" `isInfixOf`) #if __GLASGOW_HASKELL__ >= 708 it "works with modules using PatternSynonyms" $ do withDirectory_ "test/data/pattern-synonyms" $ do - res <- runID $ checkSyntax ["B.hs"] + res <- runD $ checkSyntax ["B.hs"] res `shouldSatisfy` ("B.hs:6:9:Warning:" `isPrefixOf`) #endif it "works with foreign exports" $ do - withDirectory_ "test/data" $ do - res <- runID $ checkSyntax ["ForeignExport.hs"] + withDirectory_ "test/data/foreign-export" $ do + res <- runD $ checkSyntax ["ForeignExport.hs"] res `shouldBe` "" context "when no errors are found" $ do it "doesn't output an empty line" $ do - withDirectory_ "test/data/ghc-mod-check/Data" $ do - res <- runID $ checkSyntax ["Foo.hs"] + withDirectory_ "test/data/ghc-mod-check/lib/Data" $ do + res <- runD $ checkSyntax ["Foo.hs"] res `shouldBe` "" + +#if __GLASGOW_HASKELL__ >= 708 +-- See https://github.com/kazu-yamamoto/ghc-mod/issues/507 + it "emits warnings generated in GHC's desugar stage" $ do + withDirectory_ "test/data/check-missing-warnings" $ do + res <- runD $ checkSyntax ["DesugarWarnings.hs"] + res `shouldBe` "DesugarWarnings.hs:4:9:Warning: Pattern match(es) are non-exhaustive\NULIn a case alternative: Patterns not matched: _ : _\n" +#endif + + it "works with cabal builtin preprocessors" $ do + withDirectory_ "test/data/cabal-preprocessors" $ do + _ <- system "cabal clean" + _ <- system "cabal build" + res <- runD $ checkSyntax ["Main.hs"] + res `shouldBe` "Preprocessed.hsc:3:1:Warning: Top-level binding with no type signature: warning :: ()\n" diff --git a/test/CradleSpec.hs b/test/CradleSpec.hs index 60ae5ac..360b7e0 100644 --- a/test/CradleSpec.hs +++ b/test/CradleSpec.hs @@ -4,46 +4,23 @@ import Control.Applicative import Data.List (isSuffixOf) import Language.Haskell.GhcMod.Cradle import Language.Haskell.GhcMod.Types -import System.Directory (canonicalizePath,getCurrentDirectory) -import System.FilePath ((), pathSeparator) +import System.Directory (canonicalizePath) +import System.FilePath (pathSeparator) import Test.Hspec import Dir -spec :: Spec -spec = do - describe "findCradle" $ do - it "returns the current directory" $ do - withDirectory_ "/" $ do - curDir <- stripLastDot <$> canonicalizePath "/" - res <- findCradle - cradleCurrentDir res `shouldBe` curDir - cradleRootDir res `shouldBe` curDir - cradleCabalFile res `shouldBe` Nothing - cradlePkgDbStack res `shouldBe` [GlobalDb,UserDb] - - it "finds a cabal file and a sandbox" $ do - cwd <- getCurrentDirectory - withDirectory "test/data/subdir1/subdir2" $ \dir -> do - res <- relativeCradle dir <$> findCradle - cradleCurrentDir res `shouldBe` "test" "data" "subdir1" "subdir2" - cradleRootDir res `shouldBe` "test" "data" - cradleCabalFile res `shouldBe` Just ("test" "data" "cabalapi.cabal") - cradlePkgDbStack res `shouldBe` [GlobalDb, PackageDb (cwd "test/data/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d")] - - it "works even if a sandbox config file is broken" $ do - withDirectory "test/data/broken-sandbox" $ \dir -> do - res <- relativeCradle dir <$> findCradle - cradleCurrentDir res `shouldBe` "test" "data" "broken-sandbox" - cradleRootDir res `shouldBe` "test" "data" "broken-sandbox" - cradleCabalFile res `shouldBe` Just ("test" "data" "broken-sandbox" "dummy.cabal") - cradlePkgDbStack res `shouldBe` [GlobalDb, UserDb] +clean_ :: IO Cradle -> IO Cradle +clean_ f = do + crdl <- f + cleanupCradle crdl + return crdl relativeCradle :: FilePath -> Cradle -> Cradle -relativeCradle dir cradle = cradle { - cradleCurrentDir = toRelativeDir dir $ cradleCurrentDir cradle - , cradleRootDir = toRelativeDir dir $ cradleRootDir cradle - , cradleCabalFile = toRelativeDir dir <$> cradleCabalFile cradle +relativeCradle dir crdl = crdl { + cradleCurrentDir = toRelativeDir dir $ cradleCurrentDir crdl + , cradleRootDir = toRelativeDir dir $ cradleRootDir crdl + , cradleCabalFile = toRelativeDir dir <$> cradleCabalFile crdl } -- Work around GHC 7.2.2 where `canonicalizePath "/"` returns "/.". @@ -51,3 +28,38 @@ stripLastDot :: FilePath -> FilePath stripLastDot path | (pathSeparator:'.':"") `isSuffixOf` path = init path | otherwise = path + +spec :: Spec +spec = do + describe "findCradle" $ do + it "returns the current directory" $ do + withDirectory_ "/" $ do + curDir <- stripLastDot <$> canonicalizePath "/" + res <- clean_ findCradle + cradleCurrentDir res `shouldBe` curDir + cradleRootDir res `shouldBe` curDir + cradleCabalFile res `shouldBe` Nothing + + it "finds a cabal file and a sandbox" $ do + withDirectory "test/data/cabal-project/subdir1/subdir2" $ \dir -> do + res <- relativeCradle dir <$> clean_ findCradle + + cradleCurrentDir res `shouldBe` + "test/data/cabal-project/subdir1/subdir2" + + cradleRootDir res `shouldBe` "test/data/cabal-project" + + cradleCabalFile res `shouldBe` + Just ("test/data/cabal-project/cabalapi.cabal") + + it "works even if a sandbox config file is broken" $ do + withDirectory "test/data/broken-sandbox" $ \dir -> do + res <- relativeCradle dir <$> clean_ findCradle + cradleCurrentDir res `shouldBe` + "test" "data" "broken-sandbox" + + cradleRootDir res `shouldBe` + "test" "data" "broken-sandbox" + + cradleCabalFile res `shouldBe` + Just ("test" "data" "broken-sandbox" "dummy.cabal") diff --git a/test/Dir.hs b/test/Dir.hs index 3e6bae1..e105566 100644 --- a/test/Dir.hs +++ b/test/Dir.hs @@ -1,9 +1,15 @@ -module Dir where +module Dir ( + module Dir + , getCurrentDirectory + , () + ) where import Control.Exception as E import Data.List (isPrefixOf) import System.Directory -import System.FilePath (addTrailingPathSeparator) +import System.FilePath (addTrailingPathSeparator,()) + + withDirectory_ :: FilePath -> IO a -> IO a withDirectory_ dir action = bracket getCurrentDirectory diff --git a/test/FindSpec.hs b/test/FindSpec.hs index 3560997..99fe3aa 100644 --- a/test/FindSpec.hs +++ b/test/FindSpec.hs @@ -1,6 +1,7 @@ module FindSpec where import Language.Haskell.GhcMod.Find +import Control.Monad import Test.Hspec import TestUtils @@ -8,5 +9,5 @@ spec :: Spec spec = do describe "db <- loadSymbolDb" $ do it "lookupSymbol' db \"head\" contains at least `Data.List'" $ do - db <- runD loadSymbolDb - lookupSym "head" db `shouldContain` ["Data.List"] + db <- runD $ loadSymbolDb =<< (cradleTempDir `liftM` cradle) + lookupSym "head" db `shouldContain` [ModuleString "Data.List"] diff --git a/test/GhcApiSpec.hs b/test/GhcApiSpec.hs deleted file mode 100644 index 0368489..0000000 --- a/test/GhcApiSpec.hs +++ /dev/null @@ -1,29 +0,0 @@ -module GhcApiSpec where - -import Control.Applicative -import Data.List (sort) -import Language.Haskell.GhcMod.GHCApi -import Test.Hspec -import TestUtils - -import Dir - -spec :: Spec -spec = do - describe "findModule" $ do - it "finds Data.List in `base' and `haskell2010'" - $ withDirectory_ "test/data" $ runD $ do - pkgs <- findModule "Data.List" <$> ghcPkgDb - let pkgNames = pkgName `map` pkgs - liftIO $ pkgNames `shouldContain` ["base", "haskell2010"] - - describe "moduleInfo" $ do - it "works for modules from global packages (e.g. base:Data.List)" - $ withDirectory_ "test/data" $ runD $ do - Just info <- moduleInfo (Just ("base","","")) "Data.List" - liftIO $ sort (bindings info) `shouldContain` ["++"] - - it "works for local modules" - $ withDirectory_ "test/data" $ runD $ do - Just info <- moduleInfo Nothing "Baz" - liftIO $ bindings info `shouldContain` ["baz"] diff --git a/test/GhcPkgSpec.hs b/test/GhcPkgSpec.hs new file mode 100644 index 0000000..6f93404 --- /dev/null +++ b/test/GhcPkgSpec.hs @@ -0,0 +1,30 @@ +module GhcPkgSpec where + +import Control.Arrow +import Control.Applicative +import Distribution.Helper +import Language.Haskell.GhcMod.GhcPkg +import Language.Haskell.GhcMod.PathsAndFiles +import Language.Haskell.GhcMod.CabalHelper +import Language.Haskell.GhcMod.Error +import Test.Hspec +import System.Directory +import System.FilePath +import System.Process (readProcess, system) + +import Dir +import TestUtils +import Data.List + +spec :: Spec +spec = do + describe "getPackageDbStack'" $ do + it "fixes out of sync custom pkg-db stack" $ do + withDirectory_ "test/data/custom-cradle" $ do + _ <- system "cabal configure" + (s, s') <- runD $ do + Just stack <- getCustomPkgDbStack + withCabal $ do + stack' <- getPackageDbStack + return (stack, stack') + s' `shouldBe` s diff --git a/test/HomeModuleGraphSpec.hs b/test/HomeModuleGraphSpec.hs new file mode 100644 index 0000000..7e43140 --- /dev/null +++ b/test/HomeModuleGraphSpec.hs @@ -0,0 +1,178 @@ +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015 Daniel Gröber +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +{-# LANGUAGE OverloadedStrings #-} + +module HomeModuleGraphSpec where + +import Language.Haskell.GhcMod.HomeModuleGraph +import Language.Haskell.GhcMod.Target +import TestUtils + +import GHC +import qualified Data.Map as Map +import qualified Data.Set as Set +import Data.Maybe + +import Test.Hspec + +runAGhc :: [GHCOption] -> (HscEnv -> LightGhc a) -> IO a +runAGhc opts action = withLightHscEnv opts $ \env -> do + runLightGhc env $ getSession >>= action + +hmGraph :: FilePath -> [String] -> String -> IO GmModuleGraph +hmGraph dir opts mn = runAGhc opts $ \env -> liftIO $ do + runD' dir $ do + smp <- liftIO $ findModulePathSet env [mkModuleName mn] + homeModuleGraph env smp + +uhmGraph :: FilePath -> [String] -> String -> String -> GmModuleGraph -> IO GmModuleGraph +uhmGraph dir opts mn umn g = runAGhc opts $ \env -> liftIO $ do + runD' dir $ do + smp <- liftIO $ findModulePathSet env [mkModuleName mn] + usmp <- liftIO $ findModulePathSet env [mkModuleName umn] + updateHomeModuleGraph env g smp usmp + +mapMap :: (Ord k, Ord k') + => (k -> k') -> (a -> a') -> Map.Map k a -> Map.Map k' a' +mapMap fk fa = Map.mapKeys fk . Map.map fa + +mapMpFn :: (FilePath -> FilePath) -> ModulePath -> ModulePath +mapMpFn f (ModulePath mn fn) = ModulePath mn (f fn) + +mp :: ModuleName -> ModulePath +mp mn = ModulePath mn $ moduleNameString mn ++ ".hs" + +spec :: Spec +spec = do + describe "reachable" $ do + let + smp = + Set.fromList + [ mp "A" + , mp "B" + , mp "C" + , mp "D" + , mp "E" + , mp "F" + , mp "G" + , mp "H" + , mp "I" + ] + + moduleMap = mkModuleMap smp + + completeGraph = + Map.map (Set.map lookupMM) . Map.mapKeys lookupMM + + lookupMM = fromJust . flip Map.lookup moduleMap + + graph = completeGraph $ + Map.fromList + [ ("A", Set.fromList ["B"]) + , ("B", Set.fromList ["C", "D"]) + , ("C", Set.fromList ["F"]) + , ("D", Set.fromList ["E"]) + , ("E", Set.fromList []) + , ("F", Set.fromList []) + , ("G", Set.fromList []) + , ("H", Set.fromList []) + , ("I", Set.fromList []) + ] + + really_reachable = + Set.fromList + [ mp "A" + , mp "B" + , mp "C" + , mp "D" + , mp "E" + , mp "F" + ] + + g = GmModuleGraph { + gmgGraph = graph + } + + it "reachable Set.empty g == Set.empty" $ do + reachable Set.empty g `shouldBe` Set.empty + + it "lists only reachable nodes" $ do + reachable (Set.fromList [mp "A"]) g `shouldBe` really_reachable + + + describe "homeModuleGraph" $ do + it "cycles don't break it" $ do + let tdir = "test/data/home-module-graph/cycle" + g <- hmGraph tdir [] "A" + gmgGraph g `shouldBe` + Map.fromList + [ (mp "A", Set.fromList [mp "B"]) + , (mp "B", Set.fromList [mp "A"]) + ] + + it "follows imports" $ do + let tdir = "test/data/home-module-graph/indirect" + g <- hmGraph tdir [] "A" + gmgGraph g `shouldBe` + Map.fromList + [ (mp "A", Set.fromList [mp "A1", mp "A2", mp "A3"]) + , (mp "A1", Set.fromList [mp "B"]) + , (mp "A2", Set.fromList [mp "C"]) + , (mp "A3", Set.fromList [mp "B"]) + , (mp "B", Set.fromList []) + , (mp "C", Set.fromList []) + ] + + it "returns partial results on parse errors" $ do + let tdir = "test/data/home-module-graph/errors" + g <- hmGraph tdir [] "A" + gmgGraph g `shouldBe` + Map.fromList + [ (mp "A", Set.fromList [mp "A1", mp "A2", mp "A3"]) + , (mp "A1", Set.fromList []) -- parse error here + , (mp "A2", Set.fromList []) + , (mp "A3", Set.fromList [mp "B"]) + , (mp "B", Set.fromList []) + ] + + it "returns partial results on CPP errors" $ do + let tdir = "test/data/home-module-graph/cpp" + g <- hmGraph tdir [] "A" + gmgGraph g `shouldBe` + Map.fromList + [ (mp "A", Set.fromList [mp "A1", mp "A2", mp "A3"]) + , (mp "A1", Set.fromList []) -- CPP error here + , (mp "A2", Set.fromList []) + , (mp "A3", Set.fromList [mp "B"]) + , (mp "B", Set.fromList []) + ] + + describe "updateHomeModuleGraph" $ do + it "removes unreachable nodes" $ do + let tdir = "test/data/home-module-graph/indirect" + let tdir' = "test/data/home-module-graph/indirect-update" + ig <- hmGraph tdir [] "A" + g <- uhmGraph tdir' [] "A" "A2" ig + gmgGraph g `shouldBe` + Map.fromList + [ (mp "A", Set.fromList [mp "A1", mp "A2", mp "A3"]) + , (mp "A1", Set.fromList [mp "B"]) + , (mp "A2", Set.fromList []) + , (mp "A3", Set.fromList [mp "B"]) + , (mp "B", Set.fromList []) + -- C was removed + ] diff --git a/test/InfoSpec.hs b/test/InfoSpec.hs index adfcb32..6a5296c 100644 --- a/test/InfoSpec.hs +++ b/test/InfoSpec.hs @@ -9,51 +9,43 @@ import System.Environment.Executable (getExecutablePath) #else import System.Environment (getExecutablePath) #endif -import System.Exit import System.FilePath -import System.Process import Test.Hspec import TestUtils -import Dir spec :: Spec spec = do describe "types" $ do it "shows types of the expression and its outers" $ do - withDirectory_ "test/data/ghc-mod-check" $ do - res <- runD $ types "Data/Foo.hs" 9 5 - res `shouldBe` "9 5 11 40 \"Int -> a -> a -> a\"\n7 1 11 40 \"Int -> Integer\"\n" + let tdir = "test/data/ghc-mod-check" + res <- runD' tdir $ types "lib/Data/Foo.hs" 9 5 + res `shouldBe` "9 5 11 40 \"Int -> a -> a -> a\"\n7 1 11 40 \"Int -> Integer\"\n" it "works with a module using TemplateHaskell" $ do - withDirectory_ "test/data" $ do - res <- runD $ types "Bar.hs" 5 1 - res `shouldBe` unlines ["5 1 5 20 \"[Char]\""] + let tdir = "test/data/template-haskell" + res <- runD' tdir $ types "Bar.hs" 5 1 + res `shouldBe` unlines ["5 1 5 20 \"[Char]\""] it "works with a module that imports another module using TemplateHaskell" $ do - withDirectory_ "test/data" $ do - res <- runD $ types "Main.hs" 3 8 - res `shouldBe` unlines ["3 8 3 16 \"String -> IO ()\"", "3 8 3 20 \"IO ()\"", "3 1 3 20 \"IO ()\""] + let tdir = "test/data/template-haskell" + res <- runD' tdir $ types "ImportsTH.hs" 3 8 + res `shouldBe` unlines ["3 8 3 16 \"String -> IO ()\"", "3 8 3 20 \"IO ()\"", "3 1 3 20 \"IO ()\""] describe "info" $ do - it "works for non-export functions" $ do - withDirectory_ "test/data" $ do - res <- runD $ info "Info.hs" "fib" - res `shouldSatisfy` ("fib :: Int -> Int" `isPrefixOf`) + it "works for non exported functions" $ do + let tdir = "test/data/non-exported" + res <- runD' tdir $ info "Fib.hs" $ Expression "fib" + res `shouldSatisfy` ("fib :: Int -> Int" `isPrefixOf`) it "works with a module using TemplateHaskell" $ do - withDirectory_ "test/data" $ do - res <- runD $ info "Bar.hs" "foo" - res `shouldSatisfy` ("foo :: ExpQ" `isPrefixOf`) + let tdir = "test/data/template-haskell" + res <- runD' tdir $ info "Bar.hs" $ Expression "foo" + res `shouldSatisfy` ("foo :: ExpQ" `isPrefixOf`) it "works with a module that imports another module using TemplateHaskell" $ do - withDirectory_ "test/data" $ do - res <- runD $ info "Main.hs" "bar" - res `shouldSatisfy` ("bar :: [Char]" `isPrefixOf`) - - it "doesn't fail on unicode output" $ do - dir <- getDistDir - code <- rawSystem (dir "build/ghc-mod/ghc-mod") ["info", "test/data/Unicode.hs", "Unicode", "unicode"] - code `shouldSatisfy` (== ExitSuccess) + let tdir = "test/data/template-haskell" + res <- runD' tdir $ info "ImportsTH.hs" $ Expression "bar" + res `shouldSatisfy` ("bar :: [Char]" `isPrefixOf`) getDistDir :: IO FilePath getDistDir = takeDirectory . takeDirectory . takeDirectory <$> getExecutablePath diff --git a/test/LintSpec.hs b/test/LintSpec.hs index 26ca952..f451ee4 100644 --- a/test/LintSpec.hs +++ b/test/LintSpec.hs @@ -8,10 +8,10 @@ spec :: Spec spec = do describe "lint" $ do it "can detect a redundant import" $ do - res <- runD $ lint "test/data/hlint.hs" - res `shouldBe` "test/data/hlint.hs:4:8: Error: Redundant do\NULFound:\NUL do putStrLn \"Hello, world!\"\NULWhy not:\NUL putStrLn \"Hello, world!\"\n" + res <- runD $ lint "test/data/hlint/hlint.hs" + res `shouldBe` "test/data/hlint/hlint.hs:4:8: Error: Redundant do\NULFound:\NUL do putStrLn \"Hello, world!\"\NULWhy not:\NUL putStrLn \"Hello, world!\"\n" context "when no suggestions are given" $ do it "doesn't output an empty line" $ do - res <- runD $ lint "test/data/ghc-mod-check/Data/Foo.hs" + res <- runD $ lint "test/data/ghc-mod-check/lib/Data/Foo.hs" res `shouldBe` "" diff --git a/test/Main.hs b/test/Main.hs index c831354..18aa1eb 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -4,6 +4,7 @@ import Dir import Control.Exception as E import Control.Monad (void) +import Data.List import Language.Haskell.GhcMod (debugInfo) import System.Process import Test.Hspec @@ -11,22 +12,38 @@ import TestUtils main :: IO () main = do - let sandboxes = [ "test/data", "test/data/check-packageid" + let sandboxes = [ "test/data/cabal-project" + , "test/data/check-packageid" , "test/data/duplicate-pkgver/" , "test/data/broken-cabal/" ] genSandboxCfg dir = withDirectory dir $ \cwdir -> do system ("sed 's|@CWD@|" ++ cwdir ++ "|g' cabal.sandbox.config.in > cabal.sandbox.config") pkgDirs = - [ "test/data/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d" + [ "test/data/cabal-project/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d" , "test/data/check-packageid/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d" , "test/data/duplicate-pkgver/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d"] genGhcPkgCache dir = system $ "ghc-pkg recache --force -f" ++ dir + genSandboxCfg `mapM_` sandboxes genGhcPkgCache `mapM_` pkgDirs - void $ system "find test -name setup-config -name ghc-mod.cache -exec rm {} \\;" + + let caches = [ "setup-config" + , "setup-config.ghc-mod.cabal-helper" + , "setup-config.ghc-mod.cabal-components" + , "setup-config.ghc-mod.resolved-components" + , "setup-config.ghc-mod.package-options" + , "setup-config.ghc-mod.package-db-stack" + , "ghc-mod.cache" + ] + cachesFindExp :: String + cachesFindExp = unwords $ intersperse "-o " $ map ("-name "++) caches + + cleanCmd = "find test \\( "++ cachesFindExp ++" \\) -exec rm {} \\;" + + putStrLn $ "$ " ++ cleanCmd + void $ system cleanCmd void $ system "cabal --version" - putStrLn $ "ghc-mod was built with Cabal version " ++ VERSION_Cabal void $ system "ghc --version" (putStrLn =<< runD debugInfo) diff --git a/test/MonadSpec.hs b/test/MonadSpec.hs index aeae1e0..92cbdb3 100644 --- a/test/MonadSpec.hs +++ b/test/MonadSpec.hs @@ -1,39 +1,17 @@ -{-# LANGUAGE ScopedTypeVariables #-} module MonadSpec where import Test.Hspec -import Dir import TestUtils -import Control.Applicative -import Control.Exception import Control.Monad.Error.Class spec :: Spec spec = do describe "When using GhcModT in a do block" $ it "a pattern match failure causes a call to `fail` on ErrorT in the monad stack" $ do - (a, _) + (a, _h) <- runGhcModT defaultOptions $ do Just _ <- return Nothing return "hello" `catchError` (const $ fail "oh noes") a `shouldBe` (Left $ GMEString "oh noes") - - describe "runGhcModT" $ - it "complains if the cabal file fails to parse while a sandbox is present" $ withDirectory_ "test/data/broken-cabal" $ do - shouldReturnError $ runD' (gmCradle <$> ask) - - describe "gmsGet/Put" $ - it "work" $ do - (runD $ gmsPut (GhcModState Intelligent) >> gmsGet) - `shouldReturn` (GhcModState Intelligent) - - describe "liftIO" $ do - it "converts user errors to GhcModError" $ do - shouldReturnError $ - runD' $ liftIO $ throw (userError "hello") >> return "" - - it "converts a file not found exception to GhcModError" $ do - shouldReturnError $ - runD' $ liftIO $ readFile "/DOES_NOT_EXIST" >> return "" diff --git a/test/PathsAndFilesSpec.hs b/test/PathsAndFilesSpec.hs index c1b5143..760b583 100644 --- a/test/PathsAndFilesSpec.hs +++ b/test/PathsAndFilesSpec.hs @@ -1,42 +1,33 @@ -{-# LANGUAGE CPP #-} module PathsAndFilesSpec where import Language.Haskell.GhcMod.PathsAndFiles -#if __GLASGOW_HASKELL__ <= 706 -import Language.Haskell.GhcMod.Types -import Language.Haskell.GhcMod.GhcPkg -#endif import System.Directory -import System.Environment -import System.FilePath (()) +import System.FilePath import Test.Hspec +import TestUtils spec :: Spec spec = do describe "getSandboxDb" $ do --- ghc < 7.8 -#if __GLASGOW_HASKELL__ <= 706 - it "does include a sandbox with ghc < 7.8" $ do - cwd <- getCurrentDirectory - getPackageDbStack "test/data/" `shouldReturn` [GlobalDb, PackageDb $ cwd "test/data/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d"] -#endif - it "can parse a config file and extract the sandbox package-db" $ do cwd <- getCurrentDirectory - pkgDb <- getSandboxDb "test/data/" - pkgDb `shouldBe` Just (cwd "test/data/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d") + Just db <- getSandboxDb "test/data/cabal-project" + db `shouldSatisfy` isPkgDbAt (cwd "test/data/cabal-project/.cabal-sandbox") it "returns Nothing if the sandbox config file is broken" $ do getSandboxDb "test/data/broken-sandbox" `shouldReturn` Nothing - describe "getCabalFiles" $ do - it "doesn't think $HOME/.cabal is a cabal file" $ do - (getCabalFiles =<< getEnv "HOME") `shouldReturn` [] - describe "findCabalFile" $ do it "works" $ do - findCabalFile "test/data" `shouldReturn` Just "test/data/cabalapi.cabal" + findCabalFile "test/data/cabal-project" `shouldReturn` Just "test/data/cabal-project/cabalapi.cabal" it "finds cabal files in parent directories" $ do - findCabalFile "test/data/subdir1/subdir2" `shouldReturn` Just "test/data/cabalapi.cabal" + findCabalFile "test/data/cabal-project/subdir1/subdir2" `shouldReturn` Just "test/data/cabal-project/cabalapi.cabal" + + describe "findCabalSandboxDir" $ do + it "works" $ do + findCabalSandboxDir "test/data/cabal-project" `shouldReturn` Just "test/data/cabal-project" + + it "finds sandboxes in parent directories" $ do + findCabalSandboxDir "test/data/cabal-project/subdir1/subdir2" `shouldReturn` Just "test/data/cabal-project" diff --git a/test/TargetSpec.hs b/test/TargetSpec.hs new file mode 100644 index 0000000..9207b65 --- /dev/null +++ b/test/TargetSpec.hs @@ -0,0 +1,46 @@ +{-# LANGUAGE OverloadedStrings #-} +module TargetSpec where + +import Language.Haskell.GhcMod.Target +import Language.Haskell.GhcMod.Gap +import Test.Hspec + +import TestUtils + +import GHC +import Data.List +import Data.Maybe +import System.Directory +import System.FilePath + +spec :: Spec +spec = do + describe "runLightGhc" $ do + it "works at all" $ do + withLightHscEnv [] $ \env -> + runLightGhc env (return ()) `shouldReturn` () + + it "has modules in scope" $ do + withLightHscEnv [] $ \env -> + runLightGhc env $ do + dflags <- getSessionDynFlags + let i = intersect (listVisibleModuleNames dflags) + ["Control.Applicative", "Control.Arrow" + ,"Control.Exception", "GHC.Exts", "GHC.Float"] + liftIO $ i `shouldSatisfy` not . null + + it "can get module info" $ do + withLightHscEnv [] $ \env -> + runLightGhc env $ do + mdl <- findModule "Data.List" Nothing + mmi <- getModuleInfo mdl + liftIO $ isJust mmi `shouldBe` True + + + describe "resolveModule" $ do + it "Works when a module given as path uses CPP" $ do + dir <- getCurrentDirectory + let srcDirs = [dir "test/data/target/src"] + x <- withLightHscEnv [] $ \env -> runD $ do + resolveModule env srcDirs (Left $ dir "test/data/target/Cpp.hs") + liftIO $ x `shouldBe` Just (ModulePath "Cpp" $ dir "test/data/target/Cpp.hs") diff --git a/test/TestUtils.hs b/test/TestUtils.hs index a543f37..dfe0644 100644 --- a/test/TestUtils.hs +++ b/test/TestUtils.hs @@ -1,26 +1,36 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} module TestUtils ( run , runD , runD' - , runI - , runID - , runIsolatedGhcMod - , isolateCradle + , runE + , runNullLog , shouldReturnError + , isPkgDbAt + , isPkgConfDAt , module Language.Haskell.GhcMod.Monad , module Language.Haskell.GhcMod.Types ) where +import Language.Haskell.GhcMod.Logging import Language.Haskell.GhcMod.Monad +import Language.Haskell.GhcMod.Cradle import Language.Haskell.GhcMod.Types +import Control.Arrow +import Control.Applicative +import Control.Monad.Error (ErrorT, runErrorT) +import Control.Monad.Trans.Journal +import Data.List.Split +import Data.String +import System.FilePath +import System.Directory import Test.Hspec -isolateCradle :: IOish m => GhcModT m a -> GhcModT m a -isolateCradle action = - local modifyEnv $ action - where - modifyEnv e = e { gmCradle = (gmCradle e) { cradlePkgDbStack = [GlobalDb] } } +import Exception + +testLogLevel :: GmLogLevel +testLogLevel = GmDebug extract :: Show e => IO (Either e a, w) -> IO a extract action = do @@ -29,28 +39,46 @@ extract action = do Right a -> return a Left e -> error $ show e -runIsolatedGhcMod :: Options -> GhcModT IO a -> IO a -runIsolatedGhcMod opt action = do - extract $ runGhcModT opt $ isolateCradle action +withSpecCradle :: IOish m => FilePath -> (Cradle -> m a) -> m a +withSpecCradle cradledir f = + gbracket (liftIO $ findSpecCradle cradledir) (liftIO . cleanupCradle) f --- | Run GhcMod in isolated cradle with default options -runID :: GhcModT IO a -> IO a -runID = runIsolatedGhcMod defaultOptions +withGhcModEnvSpec :: IOish m => FilePath -> Options -> (GhcModEnv -> m a) -> m a +withGhcModEnvSpec dir opt f = withSpecCradle dir $ withGhcModEnv' opt f --- | Run GhcMod in isolated cradle -runI :: Options -> GhcModT IO a -> IO a -runI = runIsolatedGhcMod +runGhcModTSpec :: Options -> GhcModT IO a -> IO (Either GhcModError a, GhcModLog) +runGhcModTSpec opt action = do + dir <- getCurrentDirectory + runGhcModTSpec' dir opt action + +runGhcModTSpec' :: IOish m + => FilePath -> Options -> GhcModT m b -> m (Either GhcModError b, GhcModLog) +runGhcModTSpec' dir opt action = liftIO (canonicalizePath dir) >>= \dir' -> + withGhcModEnvSpec dir' opt $ \env -> do + first (fst <$>) <$> runGhcModT'' env defaultGhcModState + (gmSetLogLevel (logLevel opt) >> action) -- | Run GhcMod run :: Options -> GhcModT IO a -> IO a -run opt a = extract $ runGhcModT opt a +run opt a = extract $ runGhcModTSpec opt a -- | Run GhcMod with default options runD :: GhcModT IO a -> IO a -runD = extract . runGhcModT defaultOptions +runD = + extract . runGhcModTSpec defaultOptions { logLevel = testLogLevel } -runD' :: GhcModT IO a -> IO (Either GhcModError a, GhcModLog) -runD' = runGhcModT defaultOptions +runD' :: FilePath -> GhcModT IO a -> IO a +runD' dir = + extract . runGhcModTSpec' dir defaultOptions { logLevel = testLogLevel } + +runE :: ErrorT e IO a -> IO (Either e a) +runE = runErrorT + +runNullLog :: MonadIO m => JournalT GhcModLog m a -> m a +runNullLog action = do + (a,w) <- runJournalT action + liftIO $ print w + return a shouldReturnError :: Show a => IO (Either GhcModError a, GhcModLog) @@ -61,3 +89,21 @@ shouldReturnError action = do where isLeft (Left _) = True isLeft _ = False + +isPkgConfD :: FilePath -> Bool +isPkgConfD d = let + (_dir, pkgconfd) = splitFileName d + in case splitOn "-" pkgconfd of + [_arch, _platform, _compiler, _compver, "packages.conf.d"] -> True + _ -> False + +isPkgConfDAt :: FilePath -> FilePath -> Bool +isPkgConfDAt d d' | d == takeDirectory d' && isPkgConfD d' = True +isPkgConfDAt _ _ = False + +isPkgDbAt :: FilePath -> GhcPkgDb -> Bool +isPkgDbAt d (PackageDb dir) = isPkgConfDAt d dir +isPkgDbAt _ _ = False + +instance IsString ModuleName where + fromString = mkModuleName diff --git a/test/UtilsSpec.hs b/test/UtilsSpec.hs deleted file mode 100644 index ab2a46a..0000000 --- a/test/UtilsSpec.hs +++ /dev/null @@ -1,23 +0,0 @@ -module UtilsSpec where - -import Language.Haskell.GhcMod.Error -import Language.Haskell.GhcMod.Utils -import TestUtils -import Test.Hspec - -spec :: Spec -spec = do - describe "extractParens" $ do - it "extracts the part of a string surrounded by parentheses" $ do - extractParens "asdasdasd ( hello [ world ] )()() kljlkjlkjlk" `shouldBe` "( hello [ world ] )" - extractParens "[(PackageName \"template-haskell\",InstalledPackageId \"template-haskell-2.9.0.0-8e2a49468f3b663b671c437d8579cd28\"),(PackageName \"base\",InstalledPackageId \"base-4.7.0.0-e4567cc9a8ef85f78696b03f3547b6d5\"),(PackageName \"Cabal\",InstalledPackageId \"Cabal-1.18.1.3-b9a44a5b15a8bce47d40128ac326e369\")][][]" `shouldBe` "[(PackageName \"template-haskell\",InstalledPackageId \"template-haskell-2.9.0.0-8e2a49468f3b663b671c437d8579cd28\"),(PackageName \"base\",InstalledPackageId \"base-4.7.0.0-e4567cc9a8ef85f78696b03f3547b6d5\"),(PackageName \"Cabal\",InstalledPackageId \"Cabal-1.18.1.3-b9a44a5b15a8bce47d40128ac326e369\")]" - - describe "liftMonadError" $ do - it "converts IOErrors to GhcModError" $ do - shouldReturnError $ - runD' $ liftIO $ throw (userError "hello") >> return "" - - shouldReturnError $ - runD' $ liftIO $ readFile "/DOES_NOT_EXIST" >> return "" - --- readProcessWithExitCode cmd opts "" diff --git a/test/data/Unicode.hs b/test/data/Unicode.hs deleted file mode 100644 index f5d1044..0000000 --- a/test/data/Unicode.hs +++ /dev/null @@ -1,4 +0,0 @@ -module Unicode where - -unicode :: α -> α -unicode = id diff --git a/test/data/annotations/With.hs b/test/data/annotations/With.hs new file mode 100644 index 0000000..68bd38c --- /dev/null +++ b/test/data/annotations/With.hs @@ -0,0 +1,6 @@ +module Main where + +{-# ANN module ["this", "can", "be", "anything"] #-} + +main :: IO () +main = putStrLn "Hello world!" diff --git a/test/data/bad.config b/test/data/bad.config deleted file mode 100644 index 57f89ed..0000000 --- a/test/data/bad.config +++ /dev/null @@ -1 +0,0 @@ -broken diff --git a/test/data/cabal-flags/cabal-flags.cabal b/test/data/cabal-flags/cabal-flags.cabal index d133d5b..f94cb70 100644 --- a/test/data/cabal-flags/cabal-flags.cabal +++ b/test/data/cabal-flags/cabal-flags.cabal @@ -7,8 +7,7 @@ flag test-flag default: False library - build-depends: base == 4.* + build-depends: base if flag(test-flag) build-depends: Cabal >= 1.10 - diff --git a/test/data/cabal-preprocessors/Main.hs b/test/data/cabal-preprocessors/Main.hs new file mode 100644 index 0000000..dbd74c3 --- /dev/null +++ b/test/data/cabal-preprocessors/Main.hs @@ -0,0 +1,4 @@ +import Preprocessed + +main :: IO () +main = return warning diff --git a/test/data/cabal-preprocessors/Preprocessed.hsc b/test/data/cabal-preprocessors/Preprocessed.hsc new file mode 100644 index 0000000..8e34f94 --- /dev/null +++ b/test/data/cabal-preprocessors/Preprocessed.hsc @@ -0,0 +1,3 @@ +module Preprocessed where + +warning = () diff --git a/test/data/cabal-preprocessors/cabal-preprocessors.cabal b/test/data/cabal-preprocessors/cabal-preprocessors.cabal new file mode 100644 index 0000000..d0a5039 --- /dev/null +++ b/test/data/cabal-preprocessors/cabal-preprocessors.cabal @@ -0,0 +1,14 @@ +name: cabal-preprocessors +version: 0.1.0.0 +license-file: LICENSE +author: asd +maintainer: asd +build-type: Simple +cabal-version: >=1.10 + +executable cabal-preprocessors + main-is: Main.hs + build-depends: base + default-language: Haskell2010 + other-modules: Preprocessed + ghc-options: -Wall \ No newline at end of file diff --git a/test/data/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d/Cabal-1.18.1.3-2b161c6bf77657aa17e1681d83cb051b.conf b/test/data/cabal-project/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d/Cabal-1.18.1.3-2b161c6bf77657aa17e1681d83cb051b.conf similarity index 100% rename from test/data/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d/Cabal-1.18.1.3-2b161c6bf77657aa17e1681d83cb051b.conf rename to test/data/cabal-project/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d/Cabal-1.18.1.3-2b161c6bf77657aa17e1681d83cb051b.conf diff --git a/test/data/.cabal-sandbox/packages/00-index.cache b/test/data/cabal-project/.cabal-sandbox/packages/00-index.cache similarity index 100% rename from test/data/.cabal-sandbox/packages/00-index.cache rename to test/data/cabal-project/.cabal-sandbox/packages/00-index.cache diff --git a/test/data/.cabal-sandbox/packages/00-index.tar b/test/data/cabal-project/.cabal-sandbox/packages/00-index.tar similarity index 100% rename from test/data/.cabal-sandbox/packages/00-index.tar rename to test/data/cabal-project/.cabal-sandbox/packages/00-index.tar diff --git a/test/data/Baz.hs b/test/data/cabal-project/Baz.hs similarity index 100% rename from test/data/Baz.hs rename to test/data/cabal-project/Baz.hs diff --git a/test/data/Foo.hs b/test/data/cabal-project/Foo.hs similarity index 100% rename from test/data/Foo.hs rename to test/data/cabal-project/Foo.hs diff --git a/test/data/Info.hs b/test/data/cabal-project/Info.hs similarity index 100% rename from test/data/Info.hs rename to test/data/cabal-project/Info.hs diff --git a/test/data/Main.hs b/test/data/cabal-project/Main.hs similarity index 100% rename from test/data/Main.hs rename to test/data/cabal-project/Main.hs diff --git a/test/data/cabal.sandbox.config.in b/test/data/cabal-project/cabal.sandbox.config.in similarity index 57% rename from test/data/cabal.sandbox.config.in rename to test/data/cabal-project/cabal.sandbox.config.in index 5057c11..79c39e4 100644 --- a/test/data/cabal.sandbox.config.in +++ b/test/data/cabal-project/cabal.sandbox.config.in @@ -4,15 +4,15 @@ -- if you want to change the default settings for this sandbox. -local-repo: @CWD@/test/data/.cabal-sandbox/packages -logs-dir: @CWD@/test/data/.cabal-sandbox/logs -world-file: @CWD@/test/data/.cabal-sandbox/world +local-repo: @CWD@/test/data/cabal-project/.cabal-sandbox/packages +logs-dir: @CWD@/test/data/cabal-project/.cabal-sandbox/logs +world-file: @CWD@/test/data/cabal-project/.cabal-sandbox/world user-install: False -package-db: @CWD@/test/data/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d -build-summary: @CWD@/test/data/.cabal-sandbox/logs/build.log +package-db: @CWD@/test/data/cabal-project/.cabal-sandbox/i386-osx-ghc-7.6.3-packages.conf.d +build-summary: @CWD@/test/data/cabal-project/.cabal-sandbox/logs/build.log install-dirs - prefix: @CWD@/test/data/.cabal-sandbox + prefix: @CWD@/test/data/cabal-project/.cabal-sandbox bindir: $prefix/bin libdir: $prefix/lib libsubdir: $arch-$os-$compiler/$pkgid diff --git a/test/data/cabalapi.cabal b/test/data/cabal-project/cabalapi.cabal similarity index 96% rename from test/data/cabalapi.cabal rename to test/data/cabal-project/cabalapi.cabal index 443a25e..882ed03 100644 --- a/test/data/cabalapi.cabal +++ b/test/data/cabal-project/cabalapi.cabal @@ -44,7 +44,7 @@ Executable ghc-mod Paths_ghc_mod Types GHC-Options: -Wall - Build-Depends: base >= 4.0 && < 5 + Build-Depends: base , Cabal >= 1.10 , template-haskell @@ -59,7 +59,7 @@ Test-Suite spec LangSpec LintSpec ListSpec - Build-Depends: base >= 4.0 && < 5 + Build-Depends: base , Cabal >= 1.10 Source-Repository head diff --git a/test/data/subdir1/subdir2/dummy b/test/data/cabal-project/subdir1/subdir2/dummy similarity index 100% rename from test/data/subdir1/subdir2/dummy rename to test/data/cabal-project/subdir1/subdir2/dummy diff --git a/test/data/check-missing-warnings/DesugarWarnings.hs b/test/data/check-missing-warnings/DesugarWarnings.hs new file mode 100644 index 0000000..9d80559 --- /dev/null +++ b/test/data/check-missing-warnings/DesugarWarnings.hs @@ -0,0 +1,5 @@ +module Warnings (zoo) where + +zoo :: [a] -> () +zoo x = case x of + [] -> undefined diff --git a/test/data/check-test-subdir/check-test-subdir.cabal b/test/data/check-test-subdir/check-test-subdir.cabal index 75d6ee1..315b549 100644 --- a/test/data/check-test-subdir/check-test-subdir.cabal +++ b/test/data/check-test-subdir/check-test-subdir.cabal @@ -13,3 +13,4 @@ test-suite test build-depends: base == 4.* hs-source-dirs: test main-is: Main.hs + ghc-options: -Wall diff --git a/test/data/custom-cradle/custom-cradle.cabal b/test/data/custom-cradle/custom-cradle.cabal new file mode 100644 index 0000000..9ccb91b --- /dev/null +++ b/test/data/custom-cradle/custom-cradle.cabal @@ -0,0 +1,12 @@ +name: custom-cradle +version: 0.1.0.0 +homepage: asd +license-file: LICENSE +author: asd +maintainer: asd +build-type: Simple +cabal-version: >=1.10 + +library + build-depends: base + default-language: Haskell2010 \ No newline at end of file diff --git a/test/data/custom-cradle/ghc-mod.package-db-stack b/test/data/custom-cradle/ghc-mod.package-db-stack new file mode 100644 index 0000000..ce2d741 --- /dev/null +++ b/test/data/custom-cradle/ghc-mod.package-db-stack @@ -0,0 +1,5 @@ +global +user +package-db-a +package-db-b +package-db-c diff --git a/test/data/custom-cradle/package-db-a/.gitkeep b/test/data/custom-cradle/package-db-a/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/data/custom-cradle/package-db-b/.gitkeep b/test/data/custom-cradle/package-db-b/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/data/custom-cradle/package-db-c/.gitkeep b/test/data/custom-cradle/package-db-c/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/data/ForeignExport.hs b/test/data/foreign-export/ForeignExport.hs similarity index 100% rename from test/data/ForeignExport.hs rename to test/data/foreign-export/ForeignExport.hs diff --git a/test/data/ghc-mod-check/ghc-mod-check.cabal b/test/data/ghc-mod-check/ghc-mod-check.cabal index a9a6eb8..3f472e5 100644 --- a/test/data/ghc-mod-check/ghc-mod-check.cabal +++ b/test/data/ghc-mod-check/ghc-mod-check.cabal @@ -15,13 +15,12 @@ build-type: Simple cabal-version: >=1.8 library - -- exposed-modules: - -- other-modules: + HS-Source-Dirs: lib build-depends: base exposed-modules: Data.Foo executable foo Main-Is: main.hs GHC-Options: -Wall - Build-Depends: base >= 4 && < 5 + Build-Depends: base , ghc-mod-check diff --git a/test/data/ghc-mod-check/Data/Foo.hs b/test/data/ghc-mod-check/lib/Data/Foo.hs similarity index 100% rename from test/data/ghc-mod-check/Data/Foo.hs rename to test/data/ghc-mod-check/lib/Data/Foo.hs diff --git a/test/data/hlint.hs b/test/data/hlint/hlint.hs similarity index 100% rename from test/data/hlint.hs rename to test/data/hlint/hlint.hs diff --git a/test/data/home-module-graph/cpp/A.hs b/test/data/home-module-graph/cpp/A.hs new file mode 100644 index 0000000..e4f573e --- /dev/null +++ b/test/data/home-module-graph/cpp/A.hs @@ -0,0 +1,4 @@ +module A where +import A1 +import A2 +import A3 diff --git a/test/data/home-module-graph/cpp/A1.hs b/test/data/home-module-graph/cpp/A1.hs new file mode 100644 index 0000000..82f6066 --- /dev/null +++ b/test/data/home-module-graph/cpp/A1.hs @@ -0,0 +1,4 @@ +{-# LANGUAGE CPP #-} +module A1 where +#elif +import B diff --git a/test/data/home-module-graph/cpp/A2.hs b/test/data/home-module-graph/cpp/A2.hs new file mode 100644 index 0000000..bcf1470 --- /dev/null +++ b/test/data/home-module-graph/cpp/A2.hs @@ -0,0 +1 @@ +module A2 where diff --git a/test/data/home-module-graph/cpp/A3.hs b/test/data/home-module-graph/cpp/A3.hs new file mode 100644 index 0000000..616ba75 --- /dev/null +++ b/test/data/home-module-graph/cpp/A3.hs @@ -0,0 +1,2 @@ +module A3 where +import B diff --git a/test/data/home-module-graph/cpp/B.hs b/test/data/home-module-graph/cpp/B.hs new file mode 100644 index 0000000..c759bc2 --- /dev/null +++ b/test/data/home-module-graph/cpp/B.hs @@ -0,0 +1 @@ +module B where diff --git a/test/data/home-module-graph/cycle/A.hs b/test/data/home-module-graph/cycle/A.hs new file mode 100644 index 0000000..f7e8963 --- /dev/null +++ b/test/data/home-module-graph/cycle/A.hs @@ -0,0 +1,2 @@ +module A where +import B diff --git a/test/data/home-module-graph/cycle/B.hs b/test/data/home-module-graph/cycle/B.hs new file mode 100644 index 0000000..af11916 --- /dev/null +++ b/test/data/home-module-graph/cycle/B.hs @@ -0,0 +1,2 @@ +module B where +import A diff --git a/test/data/home-module-graph/errors/A.hs b/test/data/home-module-graph/errors/A.hs new file mode 100644 index 0000000..e4f573e --- /dev/null +++ b/test/data/home-module-graph/errors/A.hs @@ -0,0 +1,4 @@ +module A where +import A1 +import A2 +import A3 diff --git a/test/data/home-module-graph/errors/A1.hs b/test/data/home-module-graph/errors/A1.hs new file mode 100644 index 0000000..422e841 --- /dev/null +++ b/test/data/home-module-graph/errors/A1.hs @@ -0,0 +1,4 @@ +module A1 where +psogduapzsü9 +import B +lxäö,vLMCks diff --git a/test/data/home-module-graph/errors/A2.hs b/test/data/home-module-graph/errors/A2.hs new file mode 100644 index 0000000..bcf1470 --- /dev/null +++ b/test/data/home-module-graph/errors/A2.hs @@ -0,0 +1 @@ +module A2 where diff --git a/test/data/home-module-graph/errors/A3.hs b/test/data/home-module-graph/errors/A3.hs new file mode 100644 index 0000000..616ba75 --- /dev/null +++ b/test/data/home-module-graph/errors/A3.hs @@ -0,0 +1,2 @@ +module A3 where +import B diff --git a/test/data/home-module-graph/errors/B.hs b/test/data/home-module-graph/errors/B.hs new file mode 100644 index 0000000..c759bc2 --- /dev/null +++ b/test/data/home-module-graph/errors/B.hs @@ -0,0 +1 @@ +module B where diff --git a/test/data/home-module-graph/indirect-update/A.hs b/test/data/home-module-graph/indirect-update/A.hs new file mode 100644 index 0000000..e4f573e --- /dev/null +++ b/test/data/home-module-graph/indirect-update/A.hs @@ -0,0 +1,4 @@ +module A where +import A1 +import A2 +import A3 diff --git a/test/data/home-module-graph/indirect-update/A1.hs b/test/data/home-module-graph/indirect-update/A1.hs new file mode 100644 index 0000000..3b7e310 --- /dev/null +++ b/test/data/home-module-graph/indirect-update/A1.hs @@ -0,0 +1,2 @@ +module A1 where +import B diff --git a/test/data/home-module-graph/indirect-update/A2.hs b/test/data/home-module-graph/indirect-update/A2.hs new file mode 100644 index 0000000..bcf1470 --- /dev/null +++ b/test/data/home-module-graph/indirect-update/A2.hs @@ -0,0 +1 @@ +module A2 where diff --git a/test/data/home-module-graph/indirect-update/A3.hs b/test/data/home-module-graph/indirect-update/A3.hs new file mode 100644 index 0000000..616ba75 --- /dev/null +++ b/test/data/home-module-graph/indirect-update/A3.hs @@ -0,0 +1,2 @@ +module A3 where +import B diff --git a/test/data/home-module-graph/indirect-update/B.hs b/test/data/home-module-graph/indirect-update/B.hs new file mode 100644 index 0000000..c759bc2 --- /dev/null +++ b/test/data/home-module-graph/indirect-update/B.hs @@ -0,0 +1 @@ +module B where diff --git a/test/data/home-module-graph/indirect-update/C.hs b/test/data/home-module-graph/indirect-update/C.hs new file mode 100644 index 0000000..5831959 --- /dev/null +++ b/test/data/home-module-graph/indirect-update/C.hs @@ -0,0 +1 @@ +module C where diff --git a/test/data/home-module-graph/indirect/A.hs b/test/data/home-module-graph/indirect/A.hs new file mode 100644 index 0000000..e4f573e --- /dev/null +++ b/test/data/home-module-graph/indirect/A.hs @@ -0,0 +1,4 @@ +module A where +import A1 +import A2 +import A3 diff --git a/test/data/home-module-graph/indirect/A1.hs b/test/data/home-module-graph/indirect/A1.hs new file mode 100644 index 0000000..3b7e310 --- /dev/null +++ b/test/data/home-module-graph/indirect/A1.hs @@ -0,0 +1,2 @@ +module A1 where +import B diff --git a/test/data/home-module-graph/indirect/A2.hs b/test/data/home-module-graph/indirect/A2.hs new file mode 100644 index 0000000..8e768fb --- /dev/null +++ b/test/data/home-module-graph/indirect/A2.hs @@ -0,0 +1,2 @@ +module A2 where +import C diff --git a/test/data/home-module-graph/indirect/A3.hs b/test/data/home-module-graph/indirect/A3.hs new file mode 100644 index 0000000..616ba75 --- /dev/null +++ b/test/data/home-module-graph/indirect/A3.hs @@ -0,0 +1,2 @@ +module A3 where +import B diff --git a/test/data/home-module-graph/indirect/B.hs b/test/data/home-module-graph/indirect/B.hs new file mode 100644 index 0000000..c759bc2 --- /dev/null +++ b/test/data/home-module-graph/indirect/B.hs @@ -0,0 +1 @@ +module B where diff --git a/test/data/home-module-graph/indirect/C.hs b/test/data/home-module-graph/indirect/C.hs new file mode 100644 index 0000000..5831959 --- /dev/null +++ b/test/data/home-module-graph/indirect/C.hs @@ -0,0 +1 @@ +module C where diff --git a/test/data/Mutual1.hs b/test/data/import-cycle/Mutual1.hs similarity index 100% rename from test/data/Mutual1.hs rename to test/data/import-cycle/Mutual1.hs diff --git a/test/data/Mutual2.hs b/test/data/import-cycle/Mutual2.hs similarity index 100% rename from test/data/Mutual2.hs rename to test/data/import-cycle/Mutual2.hs diff --git a/test/data/non-exported/Fib.hs b/test/data/non-exported/Fib.hs new file mode 100644 index 0000000..f8d97f9 --- /dev/null +++ b/test/data/non-exported/Fib.hs @@ -0,0 +1,8 @@ +{-# LANGUAGE TemplateHaskell #-} -- for HscInterpreted + +module Fib () where + +fib :: Int -> Int +fib 0 = 0 +fib 1 = 1 +fib n = fib (n - 1) + fib (n - 2) diff --git a/test/data/pattern-synonyms/pattern-synonyms.cabal b/test/data/pattern-synonyms/pattern-synonyms.cabal index a9b0489..ab75969 100644 --- a/test/data/pattern-synonyms/pattern-synonyms.cabal +++ b/test/data/pattern-synonyms/pattern-synonyms.cabal @@ -1,24 +1,25 @@ --- Initial pattern-synonyms.cabal generated by cabal init. For further +-- Initial pattern-synonyms.cabal generated by cabal init. For further -- documentation, see http://haskell.org/cabal/users-guide/ name: pattern-synonyms version: 0.1.0.0 --- synopsis: --- description: --- license: +-- synopsis: +-- description: +-- license: license-file: LICENSE author: Daniel Gröber maintainer: dxld@darkboxed.org --- copyright: --- category: +-- copyright: +-- category: build-type: Simple --- extra-source-files: +-- extra-source-files: cabal-version: >=1.10 library exposed-modules: A, B - -- other-modules: + -- other-modules: other-extensions: PatternSynonyms - build-depends: base >=4.7 && <4.8 - -- hs-source-dirs: - default-language: Haskell2010 \ No newline at end of file + build-depends: base + -- hs-source-dirs: + default-language: Haskell2010 + ghc-options: -Wall \ No newline at end of file diff --git a/test/data/quasi-quotes/FooQ.hs b/test/data/quasi-quotes/FooQ.hs new file mode 100644 index 0000000..223afa2 --- /dev/null +++ b/test/data/quasi-quotes/FooQ.hs @@ -0,0 +1,6 @@ +module FooQ (fooQ) where +import Language.Haskell.TH +import Language.Haskell.TH.Quote (QuasiQuoter(..)) + +fooQ :: QuasiQuoter +fooQ = QuasiQuoter (litE . stringL) undefined undefined undefined diff --git a/test/data/quasi-quotes/QuasiQuotes.hs b/test/data/quasi-quotes/QuasiQuotes.hs new file mode 100644 index 0000000..3ec7d09 --- /dev/null +++ b/test/data/quasi-quotes/QuasiQuotes.hs @@ -0,0 +1,6 @@ +{-# LANGUAGE QuasiQuotes #-} +module QuasiQuotes where + +import FooQ + +bar = [fooQ| foo bar baz |] diff --git a/test/data/target/Cpp.hs b/test/data/target/Cpp.hs new file mode 100644 index 0000000..5a17b7a --- /dev/null +++ b/test/data/target/Cpp.hs @@ -0,0 +1,7 @@ +{-# LANGUAGE CPP #-} +#undef NOTHING +#ifdef NOTHING +module WRONG_MODULE where +#else +module Cpp where +#endif diff --git a/test/data/Bar.hs b/test/data/template-haskell/Bar.hs similarity index 100% rename from test/data/Bar.hs rename to test/data/template-haskell/Bar.hs diff --git a/test/data/template-haskell/Foo.hs b/test/data/template-haskell/Foo.hs new file mode 100644 index 0000000..3b1bb2f --- /dev/null +++ b/test/data/template-haskell/Foo.hs @@ -0,0 +1,9 @@ +module Foo (foo, fooQ) where +import Language.Haskell.TH +import Language.Haskell.TH.Quote (QuasiQuoter(..)) + +foo :: ExpQ +foo = stringE "foo" + +fooQ :: QuasiQuoter +fooQ = QuasiQuoter (litE . stringL) undefined undefined undefined diff --git a/test/data/template-haskell/ImportsTH.hs b/test/data/template-haskell/ImportsTH.hs new file mode 100644 index 0000000..0fd5838 --- /dev/null +++ b/test/data/template-haskell/ImportsTH.hs @@ -0,0 +1,3 @@ +import Bar (bar) + +main = putStrLn bar diff --git a/test/doctests.hs b/test/doctests.hs index b860d45..03d710f 100644 --- a/test/doctests.hs +++ b/test/doctests.hs @@ -1,12 +1,15 @@ +{-# LANGUAGE CPP #-} module Main where import Test.DocTest main :: IO () -main = doctest [ - "-package" - , "ghc" - , "-XConstraintKinds", "-XFlexibleContexts" +main = doctest + [ "-package", "ghc-" ++ VERSION_ghc + , "-package", "transformers-" ++ VERSION_transformers + , "-package", "mtl-" ++ VERSION_mtl + , "-package", "directory-" ++ VERSION_directory + , "-XScopedTypeVariables", "-XRecordWildCards", "-XNamedFieldPuns", "-XConstraintKinds", "-XFlexibleContexts", "-XDataKinds", "-XKindSignatures", "-XTypeOperators" , "-idist/build/autogen/" , "-optP-include" , "-optPdist/build/autogen/cabal_macros.h"