diff --git a/Cargo.lock b/Cargo.lock index ee1ee6f240..e327bd4e36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1959,6 +1959,7 @@ dependencies = [ "leo-disassembler", "leo-errors", "leo-interpreter", + "leo-linter", "leo-package", "leo-span", "libc", @@ -1989,6 +1990,24 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "leo-linter" +version = "3.3.1" +dependencies = [ + "indexmap", + "leo-ast", + "leo-compiler", + "leo-errors", + "leo-parser", + "leo-parser-lossless", + "leo-passes", + "leo-span", + "leo-test-framework", + "serial_test", + "snarkvm", + "walkdir", +] + [[package]] name = "leo-package" version = "3.3.1" diff --git a/Cargo.toml b/Cargo.toml index e4edd684aa..e19f10dbf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ rust-version = "1.90.0" members = [ "compiler/ast", "compiler/compiler", + "compiler/linter", "compiler/parser-lossless", "compiler/parser", "compiler/passes", @@ -65,6 +66,10 @@ version = "=3.3.1" path = "./interpreter" version = "=3.3.1" +[workspace.dependencies.leo-linter] +path = "./compiler/linter" +version = "=3.3.1" + [workspace.dependencies.leo-package] path = "./leo/package" version = "=3.3.1" @@ -195,6 +200,9 @@ workspace = true [dependencies.leo-interpreter] workspace = true +[dependencies.leo-linter] +workspace = true + [dependencies.leo-package] workspace = true diff --git a/compiler/linter/Cargo.toml b/compiler/linter/Cargo.toml new file mode 100644 index 0000000000..8d873ba3ac --- /dev/null +++ b/compiler/linter/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "leo-linter" +version = "3.3.1" +authors = [ "The Leo Team " ] +description = "Linter for Leo programming language" +homepage = "https://leo-lang.org" +repository = "https://github.com/ProvableHQ/leo" +keywords = [ + "aleo", + "cryptography", + "leo", + "programming-language", + "zero-knowledge" +] +categories = [ "compilers", "cryptography", "web-programming" ] +include = [ "Cargo.toml", "src", "LICENSE.md" ] +license = "GPL-3.0" +edition = "2024" +rust-version = "1.88.0" + +[dependencies.leo-ast] +workspace = true + +[dependencies.leo-errors] +workspace = true + +[dependencies.leo-compiler] +workspace = true + +[dependencies.leo-parser] +workspace = true + +[dependencies.leo-parser-lossless] +workspace = true + +[dependencies.leo-passes] +workspace = true + +[dependencies.leo-span] +workspace = true + +[dependencies.indexmap] +workspace = true + +[dependencies.walkdir] +workspace = true + +[dev-dependencies.snarkvm] +workspace = true + +[dev-dependencies.serial_test] +workspace = true + +[dev-dependencies.leo-test-framework] +workspace = true diff --git a/compiler/linter/LICENSE.md b/compiler/linter/LICENSE.md new file mode 100644 index 0000000000..b95c626e2a --- /dev/null +++ b/compiler/linter/LICENSE.md @@ -0,0 +1,596 @@ +GNU General Public License +========================== + +Version 3, 29 June 2007 + +Copyright © 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 General Public License is a free, copyleft license for software and other +kinds of works. + +The licenses for most software and other practical works are designed to take away +your freedom to share and change the works. By contrast, the GNU General Public +License is 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. We, the Free +Software Foundation, use the GNU General Public License for most of our software; it +applies also to any other work released this way by its authors. You can apply it to +your programs, too. + +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. + +To protect your rights, we need to prevent others from denying you these rights or +asking you to surrender the rights. Therefore, you have certain responsibilities if +you distribute copies of the software, or if you modify it: responsibilities to +respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, +you must pass on to the recipients the same freedoms that you received. You must make +sure that they, too, receive or can get the source code. And you must show them these +terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: **(1)** assert +copyright on the software, and **(2)** offer you this License giving you legal permission +to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is +no warranty for this free software. For both users' and authors' sake, the GPL +requires that modified versions be marked as changed, so that their problems will not +be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of +the software inside them, although the manufacturer can do so. This is fundamentally +incompatible with the aim of protecting users' freedom to change the software. The +systematic pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we have designed +this version of the GPL to prohibit the practice for those products. If such problems +arise substantially in other domains, we stand ready to extend this provision to +those domains in future versions of the GPL, as needed to protect the freedom of +users. + +Finally, every program is threatened constantly by software patents. States should +not allow patents to restrict development and use of software on general-purpose +computers, but in those that do, we wish to avoid the special danger that patents +applied to a free program could make it effectively proprietary. To prevent this, the +GPL assures that patents cannot be used to render the program non-free. + +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 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. Use with the GNU Affero General Public License + +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 Affero +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 special requirements of the GNU Affero General Public License, section +13, concerning interaction through a network will apply to the combination as such. + +### 14. Revised Versions of this License + +The Free Software Foundation may publish revised and/or new versions of the GNU +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 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 +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 +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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this +when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type 'show c' for details. + +The hypothetical commands `show w` and `show c` should show the appropriate parts of +the General Public License. Of course, your program's commands might be different; +for a GUI interface, you would use an “about box”. + +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 GPL, see +<>. + +The GNU General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may consider it +more useful to permit linking proprietary applications with the library. If this is +what you want to do, use the GNU Lesser General Public License instead of this +License. But first, please read +<>. diff --git a/compiler/linter/src/context.rs b/compiler/linter/src/context.rs new file mode 100644 index 0000000000..f66c85847a --- /dev/null +++ b/compiler/linter/src/context.rs @@ -0,0 +1,54 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_errors::Lint; +use leo_passes::CompilerState; + +use crate::diagnostics::DiagnosticReport; + +/// Context for the Late lint pass. +#[derive(Clone, Copy)] +pub struct LateContext<'ctx> { + #[expect(dead_code)] + state: &'ctx CompilerState, + report: &'ctx DiagnosticReport, +} + +impl<'ctx> LateContext<'ctx> { + pub fn new(report: &'ctx DiagnosticReport, state: &'ctx CompilerState) -> LateContext<'ctx> { + Self { state, report } + } + + pub fn emit_lint(&self, lint: Lint) { + self.report.emit_lint(lint); + } +} + +/// Context for the early lint pass. +#[derive(Clone, Copy)] +pub struct EarlyContext<'ctx> { + report: &'ctx DiagnosticReport, +} + +impl<'ctx> EarlyContext<'ctx> { + pub fn new(report: &'ctx DiagnosticReport) -> EarlyContext<'ctx> { + Self { report } + } + + pub fn emit_lint(&self, lint: Lint) { + self.report.emit_lint(lint); + } +} diff --git a/compiler/linter/src/diagnostics.rs b/compiler/linter/src/diagnostics.rs new file mode 100644 index 0000000000..9e14c11fe8 --- /dev/null +++ b/compiler/linter/src/diagnostics.rs @@ -0,0 +1,47 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_errors::Lint; + +use std::cell::RefCell; + +/// All the triggered lints are collected here, as we traverse +/// the syntax trees and perform various lints. +#[derive(Default)] +pub struct DiagnosticReport { + inner: RefCell, +} + +impl DiagnosticReport { + pub fn emit_lint(&self, lint: Lint) { + self.inner.borrow_mut().emit_lint(lint); + } + + pub fn consume(self) -> Vec { + self.inner.into_inner().collected + } +} + +#[derive(Default)] +struct DiagnosticReportInner { + collected: Vec, +} + +impl DiagnosticReportInner { + fn emit_lint(&mut self, lint: Lint) { + self.collected.push(lint); + } +} diff --git a/compiler/linter/src/lib.rs b/compiler/linter/src/lib.rs new file mode 100644 index 0000000000..add5c04ad1 --- /dev/null +++ b/compiler/linter/src/lib.rs @@ -0,0 +1,26 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +mod context; +mod diagnostics; +mod linter; +mod lints; +mod passes; +#[cfg(test)] +mod test; + +pub use linter::Linter; +pub use passes::*; diff --git a/compiler/linter/src/linter.rs b/compiler/linter/src/linter.rs new file mode 100644 index 0000000000..a2013096a4 --- /dev/null +++ b/compiler/linter/src/linter.rs @@ -0,0 +1,229 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use std::{ffi::OsStr, fs, path::Path}; + +use indexmap::{IndexMap, IndexSet}; +use leo_ast::{NetworkName, Stub}; +use leo_errors::{CompilerError, Handler, LeoError, Result}; +use leo_parser::{ + conversions::{to_main, to_module}, + parse_cst, +}; +use leo_parser_lossless::SyntaxNode; +use leo_passes::{ + CompilerState, + Pass, + PathResolution, + StaticAnalyzing, + SymbolTableCreation, + TypeChecking, + TypeCheckingInput, +}; +use leo_span::{Symbol, source_map::FileName, with_session_globals}; +use walkdir::WalkDir; + +use crate::{ + diagnostics::DiagnosticReport, + passes::{ + early::{EarlyLinting, EarlyLintingInput}, + late::LateLinting, + }, +}; + +/// The primary entry point of the Leo linter. +pub struct Linter { + state: CompilerState, + program_name: Option, + import_stubs: IndexMap, +} + +impl Linter { + pub fn new( + program_name: Option, + handler: Handler, + is_test: bool, + import_stubs: IndexMap, + network: NetworkName, + ) -> Linter { + let state = CompilerState { is_test, handler, network, ..Default::default() }; + Linter { state, program_name, import_stubs } + } + + pub fn lint_leo_source_directory( + &mut self, + entry_file_path: impl AsRef, + modules_directory: Option>, + ) -> Result<()> { + // Walk all files under source_directory recursively, excluding the main source file itself. + let files = if let Some(source_directory) = modules_directory { + WalkDir::new(source_directory) + .into_iter() + .filter_map(Result::ok) + .filter(|e| { + e.file_type().is_file() + && e.path() != entry_file_path.as_ref() + && e.path().extension() == Some(OsStr::new("leo")) + }) + .collect::>() + } else { + Vec::new() + }; + + let main = if entry_file_path.as_ref().try_exists().map_err(|e| LeoError::Anyhow(e.into()))? { + // Read the contents of the main source file. + let source = fs::read_to_string(&entry_file_path) + .map_err(|e| CompilerError::file_read_error(entry_file_path.as_ref().display().to_string(), e))?; + Some((source, FileName::Real(entry_file_path.as_ref().to_owned()))) + } else { + None + }; + + let mut module_sources = Vec::new(); // Keep Strings alive for valid borrowing + let mut modules = Vec::new(); // Parsed (source, filename) tuples for compilation + + // Read all module files and store their contents + for file in &files { + let source = fs::read_to_string(file.path()) + .map_err(|e| CompilerError::file_read_error(file.path().display().to_string(), e))?; + module_sources.push(source); // Keep the String alive + } + + // Create tuples of (&str, FileName) for the compiler + for (i, file) in files.iter().enumerate() { + let source = &module_sources[i]; // Borrow from the alive String + modules.push((&source[..], FileName::Real(file.path().into()))); + } + + self.lint(main.as_ref().map(|(s, f)| (s.as_str(), f.clone())), modules.as_slice()) + } + + pub(crate) fn lint(&mut self, source: Option<(&str, FileName)>, modules: &[(&str, FileName)]) -> Result<()> { + // Register the source in the source map. + let source_file = with_session_globals(|s| source.map(|source| s.source_map.new_source(source.0, source.1))); + + // Register the sources of all the modules in the source map. + let modules = modules + .iter() + .map(|(source, filename)| with_session_globals(|s| s.source_map.new_source(source, filename.clone()))) + .collect::>(); + + let parse_tree = parse_cst(&self.state.handler, source_file.as_deref(), &modules)?; + + let report = + self.lint_early(parse_tree.0.as_ref(), parse_tree.1.iter().map(|m| &m.1).collect::>().as_slice())?; + + let program_name = if let Some(main_tree) = parse_tree.0 { + let program = to_main(&main_tree, &self.state.node_builder, &self.state.handler)?; + let program_name = *program.program_scopes.first().unwrap().0; + self.state.ast.ast = program; + program_name + } else { + Symbol::default() + }; + + for (key, module_tree) in parse_tree.1 { + let module_ast = + to_module(&module_tree, &self.state.node_builder, program_name, key.clone(), &self.state.handler)?; + self.state.ast.ast.modules.insert(key, module_ast); + } + + if let Some(program_scope) = self.state.ast.ast.program_scopes.values().next() + && self.program_name.is_none() + { + self.program_name = Some(program_scope.program_id.name.to_string()); + } + + self.add_import_stubs()?; + + self.intermediate_passes()?; + + self.lint_late(&report)?; + + for triggered_lint in report.consume() { + self.state.handler.emit_warning(triggered_lint); + } + + Ok(()) + } + + fn do_pass(&mut self, input: P::Input) -> Result { + let output = P::do_pass(input, &mut self.state)?; + Ok(output) + } + + fn intermediate_passes(&mut self) -> Result<()> { + let type_checking_config = TypeCheckingInput::new(self.state.network); + + self.do_pass::(())?; + + self.do_pass::(())?; + + self.do_pass::(type_checking_config.clone())?; + + self.do_pass::(())?; + + Ok(()) + } + + fn lint_early(&mut self, main_node: Option<&SyntaxNode>, module_nodes: &[&SyntaxNode]) -> Result { + let input = EarlyLintingInput { module_trees: module_nodes, program_tree: main_node }; + self.do_pass::(input) + } + + fn lint_late(&mut self, report: &DiagnosticReport) -> Result<()> { + self.do_pass::(report)?; + Ok(()) + } + + /// Merge the imported stubs which are dependencies of the current program into the AST + /// in topological order. + pub fn add_import_stubs(&mut self) -> Result<()> { + let mut explored = IndexSet::::new(); + let mut to_explore: Vec = self.state.ast.ast.imports.keys().cloned().collect(); + + while let Some(import) = to_explore.pop() { + explored.insert(import); + if let Some(stub) = self.import_stubs.get(&import) { + for new_import_id in stub.imports.iter() { + if !explored.contains(&new_import_id.name.name) { + to_explore.push(new_import_id.name.name); + } + } + } else { + if cfg!(test) { + return Ok(()); + } + return Err(CompilerError::imported_program_not_found( + self.program_name.as_ref().unwrap(), + import, + self.state.ast.ast.imports[&import].1, + ) + .into()); + } + } + + // Iterate in the order of `import_stubs` to make sure they + // stay topologically sorted. + self.state.ast.ast.stubs = self + .import_stubs + .iter() + .filter(|(symbol, _stub)| explored.contains(*symbol)) + .map(|(symbol, stub)| (*symbol, stub.clone())) + .collect(); + Ok(()) + } +} diff --git a/compiler/linter/src/lints/binary.rs b/compiler/linter/src/lints/binary.rs new file mode 100644 index 0000000000..69f90b0103 --- /dev/null +++ b/compiler/linter/src/lints/binary.rs @@ -0,0 +1,113 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_ast::{BinaryExpression, BinaryOperation, Expression, Node}; +use leo_errors::Lint; + +use crate::{context::LateContext, passes::LateLintPass}; + +/// This linter performs linting checks on various common +/// code patterns for binary operators. +pub(super) struct BinaryLints<'ctx> { + context: LateContext<'ctx>, +} + +impl<'ctx> LateLintPass<'ctx> for BinaryLints<'ctx> { + fn new(context: LateContext<'ctx>) -> Box + 'ctx> { + Box::new(Self { context }) + } + + fn get_name(&self) -> &str { + "binary" + } + + fn check_expression(&mut self, expr: &Expression) { + if let Expression::Binary(binary_expr) = expr { + BinaryLinter { context: self.context, input: binary_expr.as_ref() }.lint(); + } + } +} + +struct BinaryLinter<'ctx> { + context: LateContext<'ctx>, + input: &'ctx BinaryExpression, +} + +impl BinaryLinter<'_> { + fn lint(&self) { + self.addition_with_zero(); + self.divison_by_one(); + self.multiplication_by_one(); + self.divison_by_zero(); + self.irrefutable_pattern(); + } + + fn addition_with_zero(&self) { + if [BinaryOperation::Add, BinaryOperation::AddWrapped].contains(&self.input.op) { + let name = if matches!(self.input.left.as_u32(), Some(0)) { + &self.input.right + } else if matches!(self.input.right.as_u32(), Some(0)) { + &self.input.left + } else { + return; + }; + + self.context.emit_lint(Lint::identity_op(name, self.input.span())); + } + } + + fn multiplication_by_one(&self) { + if [BinaryOperation::Mul, BinaryOperation::MulWrapped].contains(&self.input.op) { + let name = if matches!(self.input.left.as_u32(), Some(1)) { + &self.input.right + } else if matches!(self.input.right.as_u32(), Some(1)) { + &self.input.left + } else { + return; + }; + + self.context.emit_lint(Lint::identity_op(name, self.input.span())); + } + } + + fn divison_by_one(&self) { + if [BinaryOperation::Div, BinaryOperation::DivWrapped].contains(&self.input.op) + && matches!(self.input.right.as_u32(), Some(1)) + { + self.context.emit_lint(Lint::identity_op(&self.input.left, self.input.span())); + } + } + + fn divison_by_zero(&self) { + if [BinaryOperation::Div, BinaryOperation::DivWrapped].contains(&self.input.op) + && matches!(self.input.right.as_u32(), Some(0)) + { + self.context.emit_lint(Lint::divison_by_zero(self.input.span())); + } + } + + fn irrefutable_pattern(&self) { + if let (Expression::Literal(left), Expression::Literal(right)) = (&self.input.left, &self.input.right) { + match self.input.op { + BinaryOperation::Eq if left.variant == right.variant => {} + BinaryOperation::Neq if left.variant != right.variant => {} + _ => return, + } + + self.context.emit_lint(Lint::irrefutable_pattern(self.input.span())); + } + } +} diff --git a/compiler/linter/src/lints/duplicate_imports.rs b/compiler/linter/src/lints/duplicate_imports.rs new file mode 100644 index 0000000000..604b98be1d --- /dev/null +++ b/compiler/linter/src/lints/duplicate_imports.rs @@ -0,0 +1,60 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use indexmap::IndexMap; +use leo_errors::Lint; +use leo_parser_lossless::{SyntaxKind, SyntaxNode}; +use leo_span::Span; + +use crate::{context::EarlyContext, passes::EarlyLintPass}; + +/// A lint to check for accidental duplicate imports in leo programs. +pub(super) struct DuplicateImportsLint<'ctx> { + imports: IndexMap, + context: EarlyContext<'ctx>, +} + +impl<'ctx> EarlyLintPass<'ctx> for DuplicateImportsLint<'ctx> { + fn new(context: EarlyContext<'ctx>) -> Box + 'ctx> { + Box::new(Self { context, imports: Default::default() }) + } + + fn get_name(&self) -> &str { + "duplicate imports" + } + + fn check_node(&mut self, node: &SyntaxNode) { + if let SyntaxKind::Import = node.kind { + self.check_import(node); + } + } +} + +impl DuplicateImportsLint<'_> { + fn check_import(&mut self, node: &SyntaxNode<'_>) { + match node.children[1].text.strip_suffix(".aleo") { + Some(id) => { + let pid = id.to_string(); + if self.imports.contains_key(&pid) { + self.context.emit_lint(Lint::duplicate_import(node.children[1].text, node.span)); + } else { + _ = self.imports.insert(pid, node.span) + } + } + None => panic!("{} malformed import: '{}' without '.aleo' suffix", node.span, node.children[1].text), + } + } +} diff --git a/compiler/linter/src/lints/mod.rs b/compiler/linter/src/lints/mod.rs new file mode 100644 index 0000000000..f61d0ec08d --- /dev/null +++ b/compiler/linter/src/lints/mod.rs @@ -0,0 +1,51 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +mod binary; +mod duplicate_imports; +mod semantics; + +mod unary; +mod unnecessary_braces; +mod unnecessary_paranthesis; +mod zero_prefixed_literal; + +use binary::*; +use duplicate_imports::*; +use semantics::*; + +use unary::*; +use unnecessary_braces::*; +use unnecessary_paranthesis::*; +use zero_prefixed_literal::*; + +use crate::{ + context::{EarlyContext, LateContext}, + passes::*, +}; + +pub(crate) fn get_early_lints<'ctx>(context: EarlyContext<'ctx>) -> Vec + 'ctx>> { + vec![ + DuplicateImportsLint::new(context), + UnnecessaryBraces::new(context), + UnnecessaryParens::new(context), + ZeroPrefixedLiteral::new(context), + ] +} + +pub(crate) fn get_late_lints<'ctx>(context: LateContext<'ctx>) -> Vec + 'ctx>> { + vec![BinaryLints::new(context), SemanticLinter::new(context), UnaryLints::new(context)] +} diff --git a/compiler/linter/src/lints/semantics.rs b/compiler/linter/src/lints/semantics.rs new file mode 100644 index 0000000000..acd514cc19 --- /dev/null +++ b/compiler/linter/src/lints/semantics.rs @@ -0,0 +1,270 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use indexmap::{IndexMap, IndexSet}; +use leo_ast::{AstVisitor, Block, Constructor, DefinitionStatement, Expression, Function, Identifier, Statement}; +use leo_errors::Lint; +use leo_span::{Span, Symbol}; + +use crate::{context::LateContext, passes::LateLintPass}; + +/// A linter to check for various coding habits +/// that come under the `semantics` category. +pub(super) struct SemanticLinter<'ctx> { + _context: LateContext<'ctx>, + unused_variables: UnusedVariables<'ctx>, + unused_assignments: UnusedAssignments<'ctx>, +} + +impl<'ctx> LateLintPass<'ctx> for SemanticLinter<'ctx> { + fn new(_context: LateContext<'ctx>) -> Box + 'ctx> { + Box::new(Self { + _context, + unused_variables: UnusedVariables { context: _context, block_stack: vec![] }, + unused_assignments: UnusedAssignments { + context: _context, + alive: Default::default(), + dead: Default::default(), + }, + }) + } + + fn get_name(&self) -> &str { + "semantics" + } + + fn check_expression(&mut self, expr: &Expression) { + self.unused_variables.check_expression(expr); + } + + fn check_statement(&mut self, statement: &Statement) { + self.unused_variables.check_statement(statement); + } + + fn check_block(&mut self, block: &Block) { + self.unused_variables.check_block(block); + } + + fn check_block_post(&mut self, block: &Block) { + self.unused_variables.check_block_post(block); + } + + fn check_constructor(&mut self, constructor: &Constructor) { + self.unused_assignments.check_constructor(constructor); + } + + fn check_function(&mut self, function: &Function) { + self.unused_assignments.check_function(function); + } +} + +/// A lint to check for unused variables in the leo programs. +struct UnusedVariables<'ctx> { + context: LateContext<'ctx>, + block_stack: Vec>, +} + +impl<'ctx> LateLintPass<'ctx> for UnusedVariables<'ctx> { + fn new(context: LateContext<'ctx>) -> Box + 'ctx> { + Box::new(Self { context, block_stack: vec![] }) + } + + fn get_name(&self) -> &str { + "unused variables" + } + + fn check_block(&mut self, _block: &Block) { + self.block_stack.push(Default::default()); + } + + fn check_block_post(&mut self, _block: &Block) { + for unused_var in self.block_stack.pop().unwrap() { + self.context.emit_lint(Lint::unused_variable(unused_var.0, unused_var.1)); + } + } + + fn check_expression(&mut self, expr: &Expression) { + if let Expression::Path(path) = expr { + self.block_stack.last_mut().unwrap().swap_remove(&path.identifier().name); + } + } + + fn check_statement(&mut self, statement: &Statement) { + if let Statement::Definition(def) = statement { + match &def.place { + leo_ast::DefinitionPlace::Single(identifier) => { + self.block_stack.last_mut().unwrap().insert(identifier.name, identifier.span); + } + leo_ast::DefinitionPlace::Multiple(identifiers) => { + identifiers.iter().for_each(|id| _ = self.block_stack.last_mut().unwrap().insert(id.name, id.span)); + } + } + } + } +} + +/// A lint to check for the unused assignments or dead variables in the leo programs. +struct UnusedAssignments<'ctx> { + context: LateContext<'ctx>, + alive: IndexSet, + dead: IndexSet<(Symbol, Span)>, +} + +impl<'ctx> LateLintPass<'ctx> for UnusedAssignments<'ctx> { + fn new(context: LateContext<'ctx>) -> Box + 'ctx> { + Box::new(Self { context, alive: Default::default(), dead: Default::default() }) + } + + fn get_name(&self) -> &str { + "unused assignments" + } + + fn check_function(&mut self, function: &leo_ast::Function) { + self.alive.clear(); + self.dead.clear(); + self.visit_block(&function.block); + for dead in self.dead.as_slice() { + self.context.emit_lint(Lint::unused_assignments(dead.0, dead.1)); + } + self.alive.clear(); + self.dead.clear(); + } + + fn check_constructor(&mut self, constructor: &leo_ast::Constructor) { + self.alive.clear(); + self.dead.clear(); + self.visit_block(&constructor.block); + for dead in self.dead.as_slice() { + self.context.emit_lint(Lint::unused_assignments(dead.0, dead.1)); + } + self.alive.clear(); + self.dead.clear(); + } +} + +impl AstVisitor for UnusedAssignments<'_> { + type AdditionalInput = bool; + type Output = (); + + fn visit_block(&mut self, input: &Block) { + input.statements.iter().rev().for_each(|stmt| self.visit_statement(stmt)); + } + + fn visit_statement(&mut self, input: &Statement) { + match input { + Statement::Assert(assert_statement) => self.visit_assert(assert_statement), + Statement::Assign(assign_statement) => self.visit_assign(assign_statement), + Statement::Block(block) => self.visit_block(block), + Statement::Conditional(conditional_statement) => { + let alive = self.alive.clone(); + let dead = self.dead.clone(); + self.visit_block(&conditional_statement.then); + let then_alive = std::mem::replace(&mut self.alive, alive); + let mut then_dead = std::mem::replace(&mut self.dead, dead); + if let Some(otherwise) = &conditional_statement.otherwise { + self.visit_statement(otherwise); + } + + self.alive.extend(then_alive); + + if then_dead.len() < self.dead.len() { + self.dead.retain(|dead| then_dead.contains(dead)); + } else { + then_dead.retain(|dead| self.dead.contains(dead)); + self.dead = then_dead; + } + } + Statement::Const(const_declaration) => self.visit_const(const_declaration), + Statement::Definition(definition_statement) => { + match &definition_statement.place { + leo_ast::DefinitionPlace::Single(identifier) => { + self.check_and_insert(*identifier); + } + leo_ast::DefinitionPlace::Multiple(identifiers) => { + for identifier in identifiers { + self.check_and_insert(*identifier); + } + } + } + if let Some(ty) = definition_statement.type_.as_ref() { + self.visit_type(ty) + } + self.visit_expression(&definition_statement.value, &Default::default()); + } + Statement::Expression(expression_statement) => self.visit_expression_statement(expression_statement), + Statement::Iteration(iteration_statement) => { + let mut alive_after = self.alive.clone(); + let mut dead_after = self.dead.clone(); + let statement = DefinitionStatement { + place: leo_ast::DefinitionPlace::Single(iteration_statement.variable), + type_: None, + value: Expression::default(), + span: iteration_statement.span, + id: 0, + } + .into(); + let block = Block { + statements: vec![statement] + .into_iter() + .chain(iteration_statement.block.statements.clone()) + .collect(), + ..iteration_statement.block + }; + loop { + self.visit_block(&block); + let body_in = self.alive.clone(); + let mut new_alive_after = alive_after.clone(); + new_alive_after.extend(body_in); + if new_alive_after == alive_after { + self.alive = new_alive_after; + break; + } + + alive_after = new_alive_after; + self.dead = dead_after.clone(); + } + + if dead_after.len() < self.dead.len() { + self.dead.retain(|dead| dead_after.contains(dead)); + } else { + dead_after.retain(|dead| self.dead.contains(dead)); + self.dead = dead_after; + } + } + Statement::Return(return_statement) => self.visit_return(return_statement), + } + } + + fn visit_assign(&mut self, input: &leo_ast::AssignStatement) { + self.visit_expression(&input.place, &true); + self.visit_expression(&input.value, &Default::default()); + } + + fn visit_path(&mut self, input: &leo_ast::Path, additional: &Self::AdditionalInput) -> Self::Output { + match additional { + true => self.check_and_insert(input.identifier()), + false => _ = self.alive.insert(input.identifier().name), + } + } +} + +impl UnusedAssignments<'_> { + fn check_and_insert(&mut self, identifier: Identifier) { + if !self.alive.swap_remove(&identifier.name) { + self.dead.insert((identifier.name, identifier.span)); + } + } +} diff --git a/compiler/linter/src/lints/unary.rs b/compiler/linter/src/lints/unary.rs new file mode 100644 index 0000000000..24d79718d5 --- /dev/null +++ b/compiler/linter/src/lints/unary.rs @@ -0,0 +1,59 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_ast::{Expression, LiteralVariant, Node, UnaryExpression, UnaryOperation}; +use leo_errors::Lint; + +use crate::{context::LateContext, passes::LateLintPass}; + +/// This linter performs linting on all lints that come under the +/// unary category. +pub(super) struct UnaryLints<'ctx> { + context: LateContext<'ctx>, +} + +impl<'ctx> LateLintPass<'ctx> for UnaryLints<'ctx> { + fn new(context: LateContext<'ctx>) -> Box + 'ctx> { + Box::new(Self { context }) + } + + fn get_name(&self) -> &str { + "unary" + } + + fn check_expression(&mut self, expr: &Expression) { + if let Expression::Unary(unary_expr) = expr { + self.double_negation_and_nonminimal_boolean(unary_expr); + } + } +} + +impl<'ctx> UnaryLints<'ctx> { + fn double_negation_and_nonminimal_boolean(&self, input: &UnaryExpression) { + if matches!(input.op, UnaryOperation::Not) { + if let Expression::Unary(unary) = &input.receiver + && matches!(unary.op, UnaryOperation::Not) + { + self.context.emit_lint(Lint::nonminimal_expression("double_negation", input.span())); + } + + if matches!(&input.receiver, Expression::Literal(lit) if matches!(lit.variant, LiteralVariant::Boolean(_))) + { + self.context.emit_lint(Lint::nonminimal_expression("nonminimal_boolean", input.span())); + } + } + } +} diff --git a/compiler/linter/src/lints/unnecessary_braces.rs b/compiler/linter/src/lints/unnecessary_braces.rs new file mode 100644 index 0000000000..9bd6476be2 --- /dev/null +++ b/compiler/linter/src/lints/unnecessary_braces.rs @@ -0,0 +1,59 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_errors::Lint; +use leo_parser_lossless::{StatementKind, SyntaxKind, SyntaxNode}; + +use crate::{context::EarlyContext, passes::EarlyLintPass}; + +use std::ops::Add; + +/// A lint to check for unnecessary braces. +pub(super) struct UnnecessaryBraces<'ctx> { + context: EarlyContext<'ctx>, +} + +impl<'ctx> EarlyLintPass<'ctx> for UnnecessaryBraces<'ctx> { + fn new(context: EarlyContext<'ctx>) -> Box + 'ctx> { + Box::new(Self { context }) + } + + fn get_name(&self) -> &str { + "unnecessary braces" + } + + fn check_node(&mut self, node: &SyntaxNode) { + if let Err(lint) = check_block_inner(node) { + self.context.emit_lint(lint); + } + } +} + +fn check_block_inner(node: &SyntaxNode<'_>) -> Result<(), Lint> { + if SyntaxKind::Statement(StatementKind::Block) == node.kind { + if let [left, one_stmt, right] = &node.children[..] + && one_stmt.kind == SyntaxKind::Statement(StatementKind::Block) + { + return Err(Lint::useless_braces(left.span.add(right.span))); + } + + if let [left, right] = &node.children[..] { + return Err(Lint::empty_braces(left.span.add(right.span))); + } + } + + Ok(()) +} diff --git a/compiler/linter/src/lints/unnecessary_paranthesis.rs b/compiler/linter/src/lints/unnecessary_paranthesis.rs new file mode 100644 index 0000000000..a35cc40e33 --- /dev/null +++ b/compiler/linter/src/lints/unnecessary_paranthesis.rs @@ -0,0 +1,65 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_errors::Lint; +use leo_parser_lossless::{ExpressionKind, SyntaxKind, SyntaxNode}; + +use crate::{context::EarlyContext, passes::EarlyLintPass}; + +/// A lint to check for unnecessary parentheses. +pub(super) struct UnnecessaryParens<'ctx> { + context: EarlyContext<'ctx>, +} + +impl<'ctx> EarlyLintPass<'ctx> for UnnecessaryParens<'ctx> { + fn new(context: EarlyContext<'ctx>) -> Box + 'ctx> { + Box::new(Self { context }) + } + + fn get_name(&self) -> &str { + "unneccssary parenthesis" + } + + fn check_node(&mut self, node: &SyntaxNode<'_>) { + if let Err(expr) = recursive_check_parens(0, node) { + self.context.emit_lint(Lint::useless_parens(expr.text, expr.span)) + } + } +} + +fn recursive_check_parens<'a>(depth: usize, node: &'a SyntaxNode<'a>) -> Result<(), &'a SyntaxNode<'a>> { + if let SyntaxKind::Expression(ExpressionKind::Parenthesized) = node.kind { + let [_left, expr, _right] = &node.children[..] else { + panic!("Can't happen"); + }; + return recursive_check_parens(depth + 1, expr); + } + + let mut error = false; + if depth > 1 { + error = true; + } + + if depth == 1 && matches!(node.kind, SyntaxKind::Expression(ExpressionKind::Literal(_))) { + error = true + } + + if error { + return Err(node); + } + + Ok(()) +} diff --git a/compiler/linter/src/lints/zero_prefixed_literal.rs b/compiler/linter/src/lints/zero_prefixed_literal.rs new file mode 100644 index 0000000000..6c0f87bb86 --- /dev/null +++ b/compiler/linter/src/lints/zero_prefixed_literal.rs @@ -0,0 +1,46 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_errors::Lint; +use leo_parser_lossless::{ExpressionKind, LiteralKind, SyntaxKind, SyntaxNode}; + +use crate::{context::EarlyContext, passes::EarlyLintPass}; + +/// A lint to check for numeric types starting with zero. +pub(super) struct ZeroPrefixedLiteral<'ctx> { + context: EarlyContext<'ctx>, +} + +impl<'ctx> EarlyLintPass<'ctx> for ZeroPrefixedLiteral<'ctx> { + fn new(context: EarlyContext<'ctx>) -> Box + 'ctx> { + Box::new(Self { context }) + } + + fn get_name(&self) -> &str { + "zero prefixed literal" + } + + fn check_node(&mut self, node: &SyntaxNode<'_>) { + if let SyntaxKind::Expression(ExpressionKind::Literal(LiteralKind::Integer(_) | LiteralKind::Unsuffixed)) = + node.kind + { + let text = node.text.replace("_", ""); + if text.starts_with("0") && text.parse::() != Ok(0) { + self.context.emit_lint(Lint::zero_prefixed_literal(node.text, node.span)) + } + } + } +} diff --git a/compiler/linter/src/passes/early/ast.rs b/compiler/linter/src/passes/early/ast.rs new file mode 100644 index 0000000000..7cbe3f263b --- /dev/null +++ b/compiler/linter/src/passes/early/ast.rs @@ -0,0 +1,301 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_parser_lossless::{ExpressionKind, StatementKind, SyntaxKind, SyntaxNode, TypeKind}; + +use crate::{ + check_node, + passes::early::{EarlyLintingVisitor, match_expression, match_statement, match_type}, +}; + +impl EarlyLintingVisitor<'_> { + pub(super) fn visit_expression(&mut self, node: &SyntaxNode) { + check_node!(self, node, SyntaxKind::Expression(..)); + let SyntaxKind::Expression(kind) = node.kind else { panic!("Can't happen") }; + match kind { + ExpressionKind::ArrayAccess => { + let [array, _left, index, _right] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_expression(array); + self.visit_expression(index); + } + + ExpressionKind::AssociatedFunctionCall => { + self.visit_nodes(node, match_expression, Self::visit_expression); + } + + ExpressionKind::Async => { + let [_a, block] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_statement(block); + } + + ExpressionKind::Array => self.visit_nodes(node, match_expression, Self::visit_expression), + + ExpressionKind::Binary => { + let [lhs, _op, rhs] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_expression(lhs); + self.visit_expression(rhs); + } + + ExpressionKind::Call => { + if let Some(argument_list) = + node.children.iter().find(|child| matches!(child.kind, SyntaxKind::ConstArgumentList)) + { + self.visit_const_list(argument_list); + } + + self.visit_nodes(node, match_expression, Self::visit_expression); + } + + ExpressionKind::Cast => { + let [expression, _as, type_] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_expression(expression); + self.visit_type(type_); + } + + ExpressionKind::MemberAccess => { + let [struct_, _dot, _name] = &node.children[..] else { + panic!("Can't happen."); + }; + + self.visit_expression(struct_); + } + + ExpressionKind::MethodCall => { + let [expr, _dot, _name, ..] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_expression(expr); + node.children[3..] + .iter() + .filter(|child| matches!(child.kind, SyntaxKind::Expression(..))) + .for_each(|ch| self.visit_expression(ch)); + } + + ExpressionKind::Parenthesized => { + let [_left, expr, _right] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_expression(expr); + } + + ExpressionKind::Repeat => { + let [_left, expr, _s, count, _right] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_expression(expr); + self.visit_expression(count); + } + + ExpressionKind::Struct => { + self.visit_nodes( + node, + |n| matches!(n.kind, SyntaxKind::StructMemberInitializer), + |slf, node| match &node.children[..] { + [_] => {} + [_, _, expr] => slf.visit_expression(expr), + _ => panic!("Can't happen"), + }, + ); + + let maybe_const_params = &node.children[1]; + if maybe_const_params.kind == SyntaxKind::ConstArgumentList { + self.visit_nodes(maybe_const_params, match_expression, Self::visit_expression); + } + } + + ExpressionKind::Ternary => { + let [cond, _q, if_, _c, then] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_expression(cond); + self.visit_expression(if_); + self.visit_expression(then); + } + + ExpressionKind::Tuple => self.visit_nodes(node, match_expression, Self::visit_expression), + + ExpressionKind::TupleAccess => { + let [expr, _dot, _integer] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_expression(expr); + } + + ExpressionKind::Unary => { + let [_op, operand] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_expression(operand); + } + + _ => {} + } + } + + fn visit_statement(&mut self, node: &SyntaxNode) { + self.check_node(node, match_statement); + let SyntaxKind::Statement(statement_kind) = node.kind else { panic!() }; + match statement_kind { + StatementKind::Assert => { + let [_a, _left, expr, _right, _s] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_expression(expr); + } + + StatementKind::AssertEq | StatementKind::AssertNeq => { + let [_a, _left, e0, _c, e1, _right, _s] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_expression(e0); + self.visit_expression(e1); + } + + StatementKind::Assign => { + let [lhs, _a, rhs, _s] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_expression(lhs); + self.visit_expression(rhs); + } + + StatementKind::Block => self.visit_block(node), + + StatementKind::Conditional => match &node.children[..] { + [_if, c, block] => { + self.visit_expression(c); + self.visit_block(block); + } + + [_if, c, block, _else, otherwise] => { + self.visit_expression(c); + self.visit_block(block); + self.visit_statement(otherwise); + } + + _ => panic!("Can't happen"), + }, + + StatementKind::Const => { + let [_const, _name, _c, type_, _a, rhs, _s] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_type(type_); + self.visit_expression(rhs); + } + + StatementKind::Definition => match &node.children[..] { + [_let, _name, _c, type_, _assign, e, _s] => { + self.visit_expression(e); + self.visit_type(type_); + } + [_let, _name, _assign, e, _s] => { + self.visit_expression(e); + } + children => { + self.visit_nodes(node, match_type, Self::visit_type); + + let expr = &children[children.len() - 2]; + self.visit_expression(expr); + } + }, + + StatementKind::Expression => self.visit_expression(&node.children[0]), + + StatementKind::Iteration => match &node.children[..] { + [_f, _i, _n, low, _d, hi, block] => { + self.visit_expression(low); + self.visit_expression(hi); + self.visit_block(block); + } + [_f, _i, _c, type_, _n, low, _d, hi, block] => { + self.visit_type(type_); + self.visit_expression(low); + self.visit_expression(hi); + self.visit_block(block); + } + _ => panic!("Can't happen"), + }, + + StatementKind::Return => match &node.children[..] { + [_r, e, _s] => { + self.visit_expression(e); + } + [_r, _s] => {} + _ => panic!("Can't happen"), + }, + } + } + + pub(super) fn visit_block(&mut self, node: &SyntaxNode) { + self.check_node(node, |n| n.kind == SyntaxKind::Statement(StatementKind::Block)); + self.visit_nodes(node, match_statement, Self::visit_statement); + } + + pub(super) fn visit_type(&mut self, node: &SyntaxNode) { + self.check_node(node, match_type); + let SyntaxKind::Type(type_kind) = node.kind else { panic!("Can't happen") }; + match type_kind { + TypeKind::Array => { + let [_l, type_, _s, length, _r] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_type(type_); + self.visit_expression(length); + } + TypeKind::Composite => { + let name = &node.children[0]; + if name.text.split_once(".aleo/").is_none() + && let Some(arg_list) = node.children.get(1) + { + self.visit_nodes(arg_list, match_expression, Self::visit_expression); + } + } + TypeKind::Future => { + if node.children.len() != 1 { + self.visit_nodes(node, match_type, Self::visit_type); + } + } + TypeKind::Tuple => { + self.visit_nodes(node, match_type, Self::visit_type); + } + _ => {} + } + } +} diff --git a/compiler/linter/src/passes/early/macro.rs b/compiler/linter/src/passes/early/macro.rs new file mode 100644 index 0000000000..756b3028ca --- /dev/null +++ b/compiler/linter/src/passes/early/macro.rs @@ -0,0 +1,29 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +#[macro_export] +macro_rules! check_node { + ($self:ident, $node:ident, $pat:pat) => {{ + match $node.kind { + $pat => { + $self.lints.iter_mut().for_each(|lint| lint.check_node($node)); + } + other => panic!("expected {:?}, got {:?}", stringify!($pat), other), + }; + }}; + + ($self:ident, $node:ident, $pat:pat) => {{}}; +} diff --git a/compiler/linter/src/passes/early/mod.rs b/compiler/linter/src/passes/early/mod.rs new file mode 100644 index 0000000000..53564086a0 --- /dev/null +++ b/compiler/linter/src/passes/early/mod.rs @@ -0,0 +1,61 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +mod ast; +mod r#macro; +mod program; +mod visitor; + +use leo_parser_lossless::SyntaxNode; +use leo_passes::{CompilerState, Pass}; + +use crate::{context::EarlyContext, diagnostics::DiagnosticReport, lints::get_early_lints, passes::early::visitor::*}; + +use std::marker::PhantomData; + +/// A pass to perform early lints before type checking on the raw CST. +pub struct EarlyLinting<'a>(PhantomData<&'a ()>); + +impl<'a> Pass for EarlyLinting<'a> { + type Input = EarlyLintingInput<'a>; + type Output = DiagnosticReport; + + const NAME: &'static str = "early linting"; + + fn do_pass(input: Self::Input, _state: &mut CompilerState) -> leo_errors::Result { + let report = DiagnosticReport::default(); + let context = EarlyContext::new(&report); + let lints = get_early_lints(context); + let mut visitor = EarlyLintingVisitor { lints }; + + if let Some(tree) = input.program_tree { + visitor.visit_main(tree); + } + + for tree in input.module_trees { + visitor.visit_module(tree); + } + + drop(visitor); + + Ok(report) + } +} + +pub struct EarlyLintingInput<'a> { + pub(crate) module_trees: &'a [&'a SyntaxNode<'a>], + pub(crate) program_tree: Option<&'a SyntaxNode<'a>>, +} diff --git a/compiler/linter/src/passes/early/program.rs b/compiler/linter/src/passes/early/program.rs new file mode 100644 index 0000000000..a886c76de9 --- /dev/null +++ b/compiler/linter/src/passes/early/program.rs @@ -0,0 +1,164 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_parser_lossless::{StatementKind, SyntaxKind, SyntaxNode}; + +use crate::{ + check_node, + passes::early::{EarlyLintingVisitor, match_expression, match_kind}, +}; + +impl EarlyLintingVisitor<'_> { + pub(super) fn visit_module(&mut self, node: &SyntaxNode) { + check_node!(self, node, SyntaxKind::ModuleContents); + self.visit_nodes( + node, + |_| true, + |slf, node| match node.kind { + SyntaxKind::GlobalConst => slf.visit_const(node), + SyntaxKind::Function => slf.visit_function(node), + SyntaxKind::StructDeclaration => slf.visit_composite(node), + _ => {} + }, + ); + } + + pub(super) fn visit_main(&mut self, node: &SyntaxNode) { + self.check_node(node, |n| n.kind == SyntaxKind::MainContents); + node.children + .iter() + .filter(|child| matches!(child.kind, SyntaxKind::Import)) + .for_each(|import| self.visit_import(import)); + let program_node = node.children.last().unwrap(); + self.visit_program(program_node); + } + + fn visit_program(&mut self, node: &SyntaxNode) { + self.check_node(node, match_kind(SyntaxKind::ProgramDeclaration)); + self.visit_nodes( + node, + |_| true, + |slf, node| match node.kind { + SyntaxKind::GlobalConst => slf.visit_const(node), + SyntaxKind::Function => slf.visit_function(node), + SyntaxKind::StructDeclaration => slf.visit_composite(node), + SyntaxKind::Constructor => slf.visit_constructor(node), + SyntaxKind::Mapping => slf.visit_mapping(node), + _ => {} + }, + ); + } + + fn visit_mapping(&mut self, node: &SyntaxNode) { + check_node!(self, node, SyntaxKind::Mapping); + let [_mapping, _name, _colon, key_type, _arrow, value_type, _s] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_type(key_type); + self.visit_type(value_type); + } + + fn visit_constructor(&mut self, node: &SyntaxNode) { + check_node!(self, node, SyntaxKind::Constructor); + self.visit_nodes(node, match_kind(SyntaxKind::Annotation), Self::visit_annotation); + self.visit_block(node.children.last().unwrap()); + } + + fn visit_composite(&mut self, node: &SyntaxNode) { + check_node!(self, node, SyntaxKind::StructDeclaration); + self.visit_nodes(node, match_kind(SyntaxKind::StructMemberDeclaration), |slf, node| { + check_node!(slf, node, SyntaxKind::StructMemberDeclaration); + slf.visit_type(node.children.last().unwrap()); + }); + } + + fn visit_import(&mut self, node: &SyntaxNode) { + self.check_node(node, match_kind(SyntaxKind::Import)); + } + + fn visit_const(&mut self, node: &SyntaxNode) { + self.check_node(node, match_kind(SyntaxKind::GlobalConst)); + let [_l, _ident, _colon, type_, _a, expr, _s] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_type(type_); + self.visit_expression(expr); + } + + fn visit_function(&mut self, node: &SyntaxNode) { + check_node!(self, node, SyntaxKind::Function); + self.visit_nodes( + node, + |_| true, + |slf, node| match node.kind { + SyntaxKind::Annotation => slf.visit_annotation(node), + SyntaxKind::ConstParameterList => slf.visit_const_list(node), + SyntaxKind::ParameterList => slf.visit_nodes(node, match_kind(SyntaxKind::Parameter), |slf, node| { + check_node!(slf, node, SyntaxKind::Parameter); + slf.visit_type(node.children.last().unwrap()); + }), + SyntaxKind::FunctionOutput => slf.visit_type(node.children.last().unwrap()), + SyntaxKind::FunctionOutputs => { + slf.visit_nodes(node, match_kind(SyntaxKind::FunctionOutput), Self::visit_type) + } + SyntaxKind::Statement(StatementKind::Block) => slf.visit_block(node), + _ => {} + }, + ); + } + + pub(super) fn visit_const_list(&mut self, node: &SyntaxNode) { + check_node!(self, node, SyntaxKind::ConstParameterList | SyntaxKind::ConstArgumentList); + match node.kind { + SyntaxKind::ConstParameter => { + self.visit_nodes(node, match_kind(SyntaxKind::ConstParameter), Self::visit_const_param) + } + SyntaxKind::ConstArgumentList => self.visit_nodes(node, match_expression, Self::visit_expression), + _ => panic!(), + } + } + + fn visit_const_param(&mut self, node: &SyntaxNode) { + check_node!(self, node, SyntaxKind::ConstParameter); + let [_id, _c, type_] = &node.children[..] else { + panic!("Can't happen"); + }; + + self.visit_type(type_); + } + + fn visit_annotation(&mut self, node: &SyntaxNode) { + check_node!(self, node, SyntaxKind::Annotation); + let [_at, _name, list @ ..] = &node.children[..] else { panic!("Can't happen") }; + if let Some(l) = list.first() { + self.visit_annotation_list(l); + } + } + + fn visit_annotation_list(&mut self, node: &SyntaxNode) { + check_node!(self, node, SyntaxKind::AnnotationList); + let [_l, member_list @ .., _r] = &node.children[..] else { panic!("Can't happen") }; + for member in member_list.iter().filter(|m| matches!(m.kind, SyntaxKind::AnnotationMember)) { + self.visit_annotation_member(member); + } + } + + fn visit_annotation_member(&mut self, node: &SyntaxNode) { + check_node!(self, node, SyntaxKind::AnnotationMember) + } +} diff --git a/compiler/linter/src/passes/early/visitor.rs b/compiler/linter/src/passes/early/visitor.rs new file mode 100644 index 0000000000..9f5fd79e36 --- /dev/null +++ b/compiler/linter/src/passes/early/visitor.rs @@ -0,0 +1,64 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_parser_lossless::{SyntaxKind, SyntaxNode}; + +use crate::passes::EarlyLintPass; + +pub(super) struct EarlyLintingVisitor<'ctx> { + /// All the lints that implement the behaviour for the early linting. + pub(super) lints: Vec + 'ctx>>, +} + +impl EarlyLintingVisitor<'_> { + pub(super) fn check_node(&mut self, node: &SyntaxNode, pattern: impl Fn(&SyntaxNode) -> bool) { + assert!(pattern(node)); + for lint in self.lints.iter_mut() { + lint.check_node(node); + } + } + + pub(super) fn visit_nodes( + &mut self, + node: &SyntaxNode, + filter: impl Fn(&SyntaxNode) -> bool, + mut func: impl FnMut(&mut Self, &SyntaxNode), + ) { + for node in &node.children { + if !filter(node) { + continue; + } + + func(self, node); + } + } +} + +pub(super) fn match_expression(node: &SyntaxNode) -> bool { + matches!(node.kind, SyntaxKind::Expression(..)) +} + +pub(super) fn match_statement(node: &SyntaxNode) -> bool { + matches!(node.kind, SyntaxKind::Statement(..)) +} + +pub(super) fn match_type(node: &SyntaxNode) -> bool { + matches!(node.kind, SyntaxKind::Type(..)) +} + +pub(super) fn match_kind(kind: SyntaxKind) -> impl Fn(&SyntaxNode) -> bool { + move |node| node.kind == kind +} diff --git a/compiler/linter/src/passes/late/ast.rs b/compiler/linter/src/passes/late/ast.rs new file mode 100644 index 0000000000..0e8050a344 --- /dev/null +++ b/compiler/linter/src/passes/late/ast.rs @@ -0,0 +1,83 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_ast::{AstVisitor, Block, Expression, Statement}; + +use super::visitor::LateLintingVisitor; + +impl AstVisitor for LateLintingVisitor<'_> { + type AdditionalInput = (); + type Output = (); + + fn visit_expression(&mut self, input: &Expression, additional: &Self::AdditionalInput) -> Self::Output { + for lint in &mut self.lints { + lint.check_expression(input); + } + + match input { + Expression::Array(array) => self.visit_array(array, additional), + Expression::ArrayAccess(access) => self.visit_array_access(access, additional), + Expression::AssociatedConstant(constant) => self.visit_associated_constant(constant, additional), + Expression::AssociatedFunction(function) => self.visit_associated_function(function, additional), + Expression::Async(async_) => self.visit_async(async_, additional), + Expression::Binary(binary) => self.visit_binary(binary, additional), + Expression::Call(call) => self.visit_call(call, additional), + Expression::Cast(cast) => self.visit_cast(cast, additional), + Expression::Struct(struct_) => self.visit_struct_init(struct_, additional), + Expression::Err(err) => self.visit_err(err, additional), + Expression::Path(path) => self.visit_path(path, additional), + Expression::Literal(literal) => self.visit_literal(literal, additional), + Expression::Locator(locator) => self.visit_locator(locator, additional), + Expression::MemberAccess(access) => self.visit_member_access(access, additional), + Expression::Repeat(repeat) => self.visit_repeat(repeat, additional), + Expression::Ternary(ternary) => self.visit_ternary(ternary, additional), + Expression::Tuple(tuple) => self.visit_tuple(tuple, additional), + Expression::TupleAccess(access) => self.visit_tuple_access(access, additional), + Expression::Unary(unary) => self.visit_unary(unary, additional), + Expression::Unit(unit) => self.visit_unit(unit, additional), + } + } + + fn visit_statement(&mut self, input: &Statement) { + for lint in &mut self.lints { + lint.check_statement(input); + } + + match input { + Statement::Assert(stmt) => self.visit_assert(stmt), + Statement::Assign(stmt) => self.visit_assign(stmt), + Statement::Block(stmt) => self.visit_block(stmt), + Statement::Conditional(stmt) => self.visit_conditional(stmt), + Statement::Const(stmt) => self.visit_const(stmt), + Statement::Definition(stmt) => self.visit_definition(stmt), + Statement::Expression(stmt) => self.visit_expression_statement(stmt), + Statement::Iteration(stmt) => self.visit_iteration(stmt), + Statement::Return(stmt) => self.visit_return(stmt), + } + } + + fn visit_block(&mut self, input: &Block) { + for lint in &mut self.lints { + lint.check_block(input); + } + + input.statements.iter().for_each(|stmt| self.visit_statement(stmt)); + + for lint in &mut self.lints { + lint.check_block_post(input); + } + } +} diff --git a/compiler/linter/src/passes/late/mod.rs b/compiler/linter/src/passes/late/mod.rs new file mode 100644 index 0000000000..ea355bbb04 --- /dev/null +++ b/compiler/linter/src/passes/late/mod.rs @@ -0,0 +1,50 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +mod ast; +mod program; +mod visitor; + +use std::marker::PhantomData; + +use leo_ast::ProgramVisitor; +use leo_errors::Result; +use leo_passes::{CompilerState, Pass}; + +use visitor::LateLintingVisitor; + +use crate::{context::LateContext, diagnostics::DiagnosticReport, lints::get_late_lints}; + +/// A pass to perform late lints after type checking. +pub struct LateLinting<'ctx>(PhantomData<&'ctx ()>); + +impl<'ctx> Pass for LateLinting<'ctx> { + type Input = &'ctx DiagnosticReport; + type Output = (); + + const NAME: &'static str = "late linting"; + + fn do_pass(input: Self::Input, state: &mut CompilerState) -> Result { + let ast = std::mem::take(&mut state.ast); + let context = LateContext::new(input, state); + let lints = get_late_lints(context); + let mut visitor = LateLintingVisitor { lints }; + visitor.visit_program(&ast.ast); + drop(visitor); + state.ast = ast; + Ok(()) + } +} diff --git a/compiler/linter/src/passes/late/program.rs b/compiler/linter/src/passes/late/program.rs new file mode 100644 index 0000000000..8b6daae681 --- /dev/null +++ b/compiler/linter/src/passes/late/program.rs @@ -0,0 +1,41 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_ast::{AstVisitor as _, Constructor, Function, ProgramVisitor}; + +use super::visitor::LateLintingVisitor; + +impl ProgramVisitor for LateLintingVisitor<'_> { + fn visit_function(&mut self, input: &Function) { + for lint in &mut self.lints { + lint.check_function(input); + } + + input.const_parameters.iter().for_each(|input| self.visit_type(&input.type_)); + input.input.iter().for_each(|input| self.visit_type(&input.type_)); + input.output.iter().for_each(|output| self.visit_type(&output.type_)); + self.visit_type(&input.output_type); + self.visit_block(&input.block); + } + + fn visit_constructor(&mut self, input: &Constructor) { + for lint in &mut self.lints { + lint.check_constructor(input); + } + + self.visit_block(&input.block); + } +} diff --git a/compiler/linter/src/passes/late/visitor.rs b/compiler/linter/src/passes/late/visitor.rs new file mode 100644 index 0000000000..4f6ecee2a8 --- /dev/null +++ b/compiler/linter/src/passes/late/visitor.rs @@ -0,0 +1,22 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use crate::passes::LateLintPass; + +pub(super) struct LateLintingVisitor<'ctx> { + /// All the lints that implement the behaviour for the late linting. + pub(super) lints: Vec + 'ctx>>, +} diff --git a/compiler/linter/src/passes/mod.rs b/compiler/linter/src/passes/mod.rs new file mode 100644 index 0000000000..ef2f4b032c --- /dev/null +++ b/compiler/linter/src/passes/mod.rs @@ -0,0 +1,59 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use leo_ast::{Block, Constructor, Expression, Function, Statement}; +use leo_parser_lossless::SyntaxNode; + +pub(crate) mod early; +pub(crate) mod late; + +use crate::context::{EarlyContext, LateContext}; + +/// A lint pass that is to be run late in the process after the type checking and +/// symbol table info is available to the compiler. Hence, the lints in this pass +/// have access to that information from the compiler state. +/// Note: This trait is currently minimal. We can easily add new checks as we add more lints. +pub trait LateLintPass<'ctx> { + #[expect(clippy::new_ret_no_self)] + fn new(context: LateContext<'ctx>) -> Box + 'ctx> + where + Self: Sized; + + fn get_name(&self) -> &str; + + fn check_expression(&mut self, _expr: &Expression) {} + fn check_statement(&mut self, _statement: &Statement) {} + fn check_block(&mut self, _block: &Block) {} + fn check_block_post(&mut self, _block: &Block) {} + fn check_function(&mut self, _function: &Function) {} + fn check_constructor(&mut self, _constructor: &Constructor) {} +} + +/// A lint pass that is to be run early in the process before the type checking and +/// symbol table creation. Hence, it doesn't have the type information available. +/// This pass expects and works on a raw concrete syntax tree from the parser. +/// It is intented to be used for lints that cannot be performed later in the pipeline +/// due to loss of revelant information when converting to an abstract syntax tree. +pub trait EarlyLintPass<'ctx> { + #[expect(clippy::new_ret_no_self)] + fn new(context: EarlyContext<'ctx>) -> Box + 'ctx> + where + Self: Sized; + + fn get_name(&self) -> &str; + + fn check_node(&mut self, node: &SyntaxNode); +} diff --git a/compiler/linter/src/test.rs b/compiler/linter/src/test.rs new file mode 100644 index 0000000000..5d78204db5 --- /dev/null +++ b/compiler/linter/src/test.rs @@ -0,0 +1,107 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use std::path::PathBuf; + +use leo_ast::NetworkName; +use leo_errors::{BufferEmitter, Handler}; +use leo_span::{create_session_if_not_set_then, source_map::FileName}; + +use indexmap::IndexMap; +use serial_test::serial; + +use crate::Linter; + +pub const MODULE_DELIMITER: &str = "// --- Next Module:"; + +/// Splits a monolithic source file into a main script and submodules. +/// Modules are separated using the `MODULE_DELIMITER` followed by the module path. +/// +/// Returns: +/// - `Option<(&str, FileName)>`: The main module's source (if present) +/// - `Vec<(&str, FileName)>`: A list of module source strings and their corresponding paths +#[expect(clippy::type_complexity)] +fn split_modules(source: &str) -> (Option<(&str, FileName)>, Vec<(&str, FileName)>) { + let mut main_source = None; + let mut modules = Vec::new(); + + let mut current_module_path: Option = None; + let mut current_start = 0; + + for (i, line) in source.lines().enumerate() { + let line_start = source.lines().take(i).map(|s| s.len() + 1).sum(); // byte offset + + if let Some(rest) = line.strip_prefix(MODULE_DELIMITER) { + // End the previous block + let block = &source[current_start..line_start]; + + if let Some(path) = current_module_path.take() { + modules.push((block, path)); + } else { + main_source = Some(block); + } + + // Start new module + let trimmed_path = rest.trim().trim_end_matches(" --- //"); + current_module_path = Some(PathBuf::from(trimmed_path)); + current_start = line_start + line.len() + 1; + } + } + + // Handle final block + let last_block = &source[current_start..]; + if let Some(path) = current_module_path { + modules.push((last_block, path)); + } else { + main_source = Some(last_block); + } + + // Prepare module references for compiler + let module_refs: Vec<(&str, FileName)> = + modules.iter().map(|(src, path)| (*src, FileName::Custom(path.to_string_lossy().into()))).collect(); + + let filename = FileName::Custom("linter-test".into()); + (main_source.filter(|s| !s.trim().is_empty()).map(|m| (m, filename)), module_refs) +} + +fn run_test(test: &str, handler: &Handler) -> Result<(), ()> { + let (main, modules) = split_modules(test); + + let mut linter = Linter::new(None, handler.clone(), false, IndexMap::new(), NetworkName::TestnetV0); + handler.extend_if_error(linter.lint(main, modules.as_slice()))?; + + if handler.err_count() != 0 { + return Err(()); + } + + Ok(()) +} + +fn runner(source: &str) -> String { + let buf = BufferEmitter::new(); + let handler = Handler::new(buf.clone()); + + create_session_if_not_set_then(|_| match run_test(source, &handler) { + Ok(_) => format!("{}", buf.extract_warnings()), + Err(_) => format!("{}{}", buf.extract_errs(), buf.extract_warnings()), + }) +} + +#[test] +#[serial] +fn test_linter() { + leo_test_framework::run_tests("linter", runner); +} diff --git a/compiler/parser/src/lib.rs b/compiler/parser/src/lib.rs index e596a28308..e6b35962d3 100644 --- a/compiler/parser/src/lib.rs +++ b/compiler/parser/src/lib.rs @@ -28,13 +28,14 @@ use itertools::Itertools as _; use leo_ast::{NetworkName, NodeBuilder}; use leo_errors::{Handler, ParserError, Result}; +use leo_parser_lossless::SyntaxNode; use leo_span::{ Symbol, source_map::{FileName, SourceFile}, sym, }; -mod conversions; +pub mod conversions; #[cfg(test)] mod test; @@ -81,18 +82,36 @@ pub fn parse( modules: &[std::rc::Rc], _network: NetworkName, ) -> Result { - let program_node = leo_parser_lossless::parse_main(handler.clone(), &source.src, source.absolute_start)?; - let mut program = conversions::to_main(&program_node, node_builder, &handler)?; + let (program_node, module_nodes) = parse_cst(&handler, Some(source), modules)?; + let mut program = conversions::to_main(&program_node.unwrap(), node_builder, &handler)?; let program_name = *program.program_scopes.first().unwrap().0; + for (key, module) in module_nodes { + let module_ast = conversions::to_module(&module, node_builder, program_name, key.clone(), &handler)?; + program.modules.insert(key, module_ast); + } + + Ok(program) +} + +#[expect(clippy::type_complexity)] +pub fn parse_cst<'a>( + handler: &Handler, + source: Option<&'a SourceFile>, + modules: &'a [std::rc::Rc], +) -> Result<(Option>, Vec<(Vec, SyntaxNode<'a>)>)> { + let main_parse_tree = + source.map(|s| leo_parser_lossless::parse_main(handler.clone(), &s.src, s.absolute_start)).transpose()?; + // Determine the root directory of the main file (for module resolution) - let root_dir = match &source.name { - FileName::Real(path) => path.parent().map(|p| p.to_path_buf()), + let root_dir = match source.map(|s| &s.name) { + Some(FileName::Real(path)) => path.parent().map(|p| p.to_path_buf()), _ => None, }; + let mut module_parse_trees = Vec::new(); for module in modules { - let node_module = leo_parser_lossless::parse_module(handler.clone(), &module.src, module.absolute_start)?; + let module_parse_tree = leo_parser_lossless::parse_module(handler.clone(), &module.src, module.absolute_start)?; if let Some(key) = compute_module_key(&module.name, root_dir.as_deref()) { // Ensure no module uses a keyword in its name for segment in &key { @@ -101,12 +120,11 @@ pub fn parse( } } - let module_ast = conversions::to_module(&node_module, node_builder, program_name, key.clone(), &handler)?; - program.modules.insert(key, module_ast); + module_parse_trees.push((key, module_parse_tree)); } } - Ok(program) + Ok((main_parse_tree, module_parse_trees)) } /// Creates a new AST from a given file path and source code text. diff --git a/errors/src/errors/linter/lints.rs b/errors/src/errors/linter/lints.rs new file mode 100644 index 0000000000..027dee4703 --- /dev/null +++ b/errors/src/errors/linter/lints.rs @@ -0,0 +1,102 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use crate::create_messages; +use std::fmt::{Debug, Display}; + +create_messages!( + #[derive(Hash, PartialEq, Eq)] + Lint, + code_mask: 11000i32, + code_prefix: "LINT", + + @formatted + identity_op { + args: (name: impl Display), + msg: format!("This operation has no effect."), + help: Some(format!("Consider reducing it to: `{name}`.")), + } + + @formatted + divison_by_zero { + args: (), + msg: format!("Attempt to divide by zero."), + help: None, + } + + @formatted + irrefutable_pattern { + args: (), + msg: format!("Irrefutable comparison: this expression will always yield true."), + help: None, + } + + @formatted + nonminimal_expression { + args: (kind: impl Display), + msg: format!("{kind}: this expression can be simplified."), + help: None, + } + + @formatted + useless_parens { + args: (replacement: impl Display), + msg: format!("Unnecessary parentheses around the expression."), + help: Some(format!("Consider replacing the expression with '{replacement}'.")), + } + + @formatted + useless_braces { + args: (), + msg: format!("Unnecessary braces around the statements."), + help: Some("Consider removing the extra braces.".to_string()), + } + + @formatted + empty_braces { + args: (), + msg: format!("Empty block statement."), + help: Some("Consider removing the block or adding statements to it.".to_string()), + } + + @formatted + unused_variable { + args: (var: impl Display), + msg: format!("unused variable `{var}`"), + help: Some(format!("if this is intentional, consider prefixing it with an underscore: `_{var}`.")), + } + + @formatted + unused_assignments { + args: (var: impl Display), + msg: format!("value assigned to `{var}` is never read."), + help: Some("maybe it is overwritten before being read?.".to_string()), + } + + @formatted + duplicate_import { + args: (import: impl Display), + msg: format!("the import `{import}` is defined multiple times"), + help: None, + } + + @formatted + zero_prefixed_literal { + args: (literal: impl Display), + msg: "literal has leading zeroes", + help: Some(format!("consider removing the leading zeroes to imporve coding practices\n\n-\t{}\n+\t{}", literal, literal.to_string().trim_start_matches("0"))), + } +); diff --git a/errors/src/errors/linter/mod.rs b/errors/src/errors/linter/mod.rs new file mode 100644 index 0000000000..86606ba777 --- /dev/null +++ b/errors/src/errors/linter/mod.rs @@ -0,0 +1,18 @@ +// Copyright (C) 2019-2025 Provable Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library 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 General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +pub mod lints; +pub use self::lints::*; diff --git a/errors/src/errors/mod.rs b/errors/src/errors/mod.rs index 5ff1df25f8..215d3abece 100644 --- a/errors/src/errors/mod.rs +++ b/errors/src/errors/mod.rs @@ -40,6 +40,9 @@ pub use self::loop_unroller::*; mod interpreter_halt; pub use self::interpreter_halt::*; +mod linter; +pub use self::linter::*; + /// Contains the Package error definitions. mod package; pub use self::package::*; @@ -171,6 +174,8 @@ pub enum LeoWarning { /// Represents a Type Checker Warning in a Leo Warning. #[error(transparent)] TypeCheckerWarning(#[from] TypeCheckerWarning), + #[error(transparent)] + LinterWarning(#[from] Lint), } impl LeoWarning { @@ -182,6 +187,7 @@ impl LeoWarning { ParserWarning(warning) => warning.warning_code(), TypeCheckerWarning(warning) => warning.warning_code(), StaticAnalyzerWarning(warning) => warning.warning_code(), + LinterWarning(lint) => lint.warning_code(), } } } diff --git a/leo/cli/cli.rs b/leo/cli/cli.rs index 05543b23dc..95d1fb72e8 100644 --- a/leo/cli/cli.rs +++ b/leo/cli/cli.rs @@ -102,6 +102,11 @@ enum Commands { #[clap(flatten)] command: LeoRemove, }, + #[clap(about = "Check leo programs and modules for common errors.")] + Check { + #[clap(flatten)] + command: LeoCheck, + }, #[clap(about = "Clean the output directory")] Clean { #[clap(flatten)] @@ -182,6 +187,7 @@ pub fn run_with_args(cli: CLI) -> Result<()> { Commands::Synthesize { command } => command.try_execute(context), Commands::Update { command } => command.try_execute(context), Commands::Upgrade { command } => command.try_execute(context), + Commands::Check { command } => command.try_execute(context), } } diff --git a/leo/cli/commands/build.rs b/leo/cli/commands/build.rs index d723cf5c31..ee90ff9e88 100644 --- a/leo/cli/commands/build.rs +++ b/leo/cli/commands/build.rs @@ -200,7 +200,7 @@ fn handle_build(command: &LeoBuild, context: Context) -> Result<. + +use super::*; + +use std::path::Path; + +use indexmap::IndexMap; +use leo_ast::{NetworkName, Stub}; +use leo_errors::{Result, UtilError}; +use leo_linter::Linter; +use leo_package::Package; +use leo_span::Symbol; +use snarkvm::prelude::{CanaryV0, MainnetV0, TestnetV0}; + +/// Perform a check on leo programs or modules +/// for common errors, mistakes, coding practices or code quality issues etc. +#[derive(Parser, Debug)] +pub struct LeoCheck { + #[clap(flatten)] + pub(crate) env_override: EnvOptions, + #[clap(flatten)] + pub(crate) build_options: BuildOptions, +} + +impl Command for LeoCheck { + type Input = (); + type Output = (); + + fn log_span(&self) -> Span { + tracing::span!(tracing::Level::INFO, "Leo") + } + + fn prelude(&self, _context: Context) -> Result { + Ok(()) + } + + fn apply(self, context: Context, _input: Self::Input) -> Result { + self.check(context) + } +} + +impl LeoCheck { + fn check(&self, context: Context) -> Result<()> { + // Get the package path and home directory. + let package_path = context.dir()?; + let home_path = context.home()?; + + let command = self; + + // Get the network, defaulting to `TestnetV0` if none is specified. + let network = match get_network(&command.env_override.network) { + Ok(network) => network, + Err(_) => { + println!("⚠️ No network specified, defaulting to 'testnet'."); + NetworkName::TestnetV0 + } + }; + + // Get the endpoint, if it is provided. + let endpoint = get_endpoint(&command.env_override.endpoint).ok(); + + let package = if command.build_options.build_tests { + Package::from_directory_with_tests( + &package_path, + &home_path, + command.build_options.no_cache, + command.build_options.no_local, + Some(network), + endpoint.as_deref(), + )? + } else { + Package::from_directory( + &package_path, + &home_path, + command.build_options.no_cache, + command.build_options.no_local, + Some(network), + endpoint.as_deref(), + )? + }; + + // Check the manifest for the compiler version. + // If it does not match, warn the user and continue. + if package.manifest.leo != env!("CARGO_PKG_VERSION") { + tracing::warn!( + "The Leo compiler version in the manifest ({}) does not match the current version ({}).", + package.manifest.leo, + env!("CARGO_PKG_VERSION") + ); + } + + let outputs_directory = package.outputs_directory(); + let build_directory = package.build_directory(); + let imports_directory = package.imports_directory(); + let source_directory = package.source_directory(); + let main_source_path = source_directory.join("main.leo"); + + for dir in [&outputs_directory, &imports_directory] { + std::fs::create_dir_all(dir).map_err(|err| { + UtilError::util_file_io_error(format_args!("Couldn't create directory {}", dir.display()), err) + })?; + } + + // Initialize error handler. + let handler = Handler::default(); + + let mut stubs = IndexMap::new(); + + for program in package.programs.iter() { + let (bytecode, _build_path) = match &program.data { + leo_package::ProgramData::Bytecode(bytecode) => { + // This was a network dependency or local .aleo dependency, and we have its bytecode. + (bytecode.clone(), imports_directory.join(format!("{}.aleo", program.name))) + } + leo_package::ProgramData::SourcePath { directory, source } => { + // This is a local dependency, so we must compile it. + // We would need to build the main directory also just for the tests, otherwise, + // it's not needed. + let build_path = if source == &main_source_path { + build_directory.join("main.aleo") + } else { + imports_directory.join(format!("{}.aleo", program.name)) + }; + + if *source == main_source_path && !command.build_options.build_tests { + continue; + } + + // Load the manifest in local dependency. + let source_dir = directory.join("src"); + let bytecode = compile_leo_source_directory( + source, // entry file + &source_dir, + program.name, + program.is_test, + &outputs_directory, + &handler, + command.build_options.clone(), + stubs.clone(), + network, + )?; + + (bytecode, build_path) + } + }; + + // Track the Stub. + let stub = match network { + NetworkName::MainnetV0 => leo_disassembler::disassemble_from_str::(program.name, &bytecode), + NetworkName::TestnetV0 => leo_disassembler::disassemble_from_str::(program.name, &bytecode), + NetworkName::CanaryV0 => leo_disassembler::disassemble_from_str::(program.name, &bytecode), + }?; + + stubs.insert(program.name, stub); + } + + for program in &package.programs { + if let leo_package::ProgramData::SourcePath { directory, source } = &program.data + && (source == &main_source_path || *directory == package.tests_directory()) + { + check_leo_source_directory( + if !program.is_test { Some(source_directory.as_path()) } else { None }, + source.as_path(), + program.name, + program.is_test, + &handler, + stubs.clone(), + network, + )? + } + } + + Ok(()) + } +} + +fn check_leo_source_directory( + source_directory: Option<&Path>, + entry_file_path: &Path, + program_name: Symbol, + is_test: bool, + handler: &Handler, + stubs: IndexMap, + network: NetworkName, +) -> Result<()> { + // Create a new instance of the Leo linter. + let mut linter = Linter::new(Some(program_name.to_string()), handler.clone(), is_test, stubs, network); + linter.lint_leo_source_directory(entry_file_path, source_directory)?; + Ok(()) +} diff --git a/leo/cli/commands/mod.rs b/leo/cli/commands/mod.rs index c7b901bda7..bf30711a1e 100644 --- a/leo/cli/commands/mod.rs +++ b/leo/cli/commands/mod.rs @@ -21,7 +21,7 @@ mod account; pub use account::Account; mod build; -pub use build::LeoBuild; +pub use build::{LeoBuild, compile_leo_source_directory}; mod clean; pub use clean::LeoClean; @@ -66,6 +66,9 @@ pub use update::LeoUpdate; pub mod upgrade; pub use upgrade::LeoUpgrade; +pub mod check; +pub use check::LeoCheck; + use super::*; use crate::cli::{helpers::context::*, query::QueryCommands}; diff --git a/tests/expectations/linter/binary.out b/tests/expectations/linter/binary.out new file mode 100644 index 0000000000..5971740302 --- /dev/null +++ b/tests/expectations/linter/binary.out @@ -0,0 +1,46 @@ +Warning [WLINT03711001]: Attempt to divide by zero. + --> linter-test:4:22 + | + 4 | let a: u32 = 5u32 / 0u32; + | ^^^^^^^^^^^ +Warning [WLINT03711000]: This operation has no effect. + --> linter-test:9:22 + | + 9 | let a: u32 = 5u32 * 1u32; + | ^^^^^^^^^^^ + | + = Consider reducing it to: `5u32`. +Warning [WLINT03711000]: This operation has no effect. + --> linter-test:14:22 + | + 14 | let a: u32 = 5u32 + 0u32; + | ^^^^^^^^^^^ + | + = Consider reducing it to: `5u32`. +Warning [WLINT03711000]: This operation has no effect. + --> linter-test:19:22 + | + 19 | let a: u32 = 5u32 / 1u32; + | ^^^^^^^^^^^ + | + = Consider reducing it to: `5u32`. +Warning [WLINT03711002]: Irrefutable comparison: this expression will always yield true. + --> linter-test:24:23 + | + 24 | let a: bool = true == true; + | ^^^^^^^^^^^^ +Warning [WLINT03711002]: Irrefutable comparison: this expression will always yield true. + --> linter-test:25:23 + | + 25 | let b: bool = true != false; + | ^^^^^^^^^^^^^ +Warning [WLINT03711002]: Irrefutable comparison: this expression will always yield true. + --> linter-test:26:23 + | + 26 | let c: bool = 4u64 == 4u64; + | ^^^^^^^^^^^^ +Warning [WLINT03711002]: Irrefutable comparison: this expression will always yield true. + --> linter-test:27:23 + | + 27 | let d: bool = 4u32 != 3u32; + | ^^^^^^^^^^^^ diff --git a/tests/expectations/linter/duplicate_imports.out b/tests/expectations/linter/duplicate_imports.out new file mode 100644 index 0000000000..54686d2468 --- /dev/null +++ b/tests/expectations/linter/duplicate_imports.out @@ -0,0 +1,10 @@ +Warning [WLINT03711009]: the import `mod.aleo` is defined multiple times + --> linter-test:4:1 + | + 4 | import mod.aleo; + | ^^^^^^^^^^^^^^^^ +Warning [WLINT03711009]: the import `mod.aleo` is defined multiple times + --> linter-test:6:1 + | + 6 | import mod.aleo; + | ^^^^^^^^^^^^^^^^ diff --git a/tests/expectations/linter/semantics.out b/tests/expectations/linter/semantics.out new file mode 100644 index 0000000000..aab1c89ea0 --- /dev/null +++ b/tests/expectations/linter/semantics.out @@ -0,0 +1,35 @@ +Warning [WLINT03711008]: value assigned to `c` is never read. + --> linter-test:7:9 + | + 7 | c = 5u32; + | ^ + | + = maybe it is overwritten before being read?. +Warning [WLINT03711008]: value assigned to `c` is never read. + --> linter-test:6:13 + | + 6 | let c = 4u32; + | ^ + | + = maybe it is overwritten before being read?. +Warning [WLINT03711008]: value assigned to `b` is never read. + --> linter-test:5:13 + | + 5 | let b = 4u32; + | ^ + | + = maybe it is overwritten before being read?. +Warning [WLINT03711008]: value assigned to `a` is never read. + --> linter-test:4:13 + | + 4 | let a = 4u32; + | ^ + | + = maybe it is overwritten before being read?. +Warning [WLINT03711007]: unused variable `b` + --> linter-test:5:13 + | + 5 | let b = 4u32; + | ^ + | + = if this is intentional, consider prefixing it with an underscore: `_b`. diff --git a/tests/expectations/linter/unary.out b/tests/expectations/linter/unary.out new file mode 100644 index 0000000000..ddcbf7aadd --- /dev/null +++ b/tests/expectations/linter/unary.out @@ -0,0 +1,15 @@ +Warning [WLINT03711003]: nonminimal_boolean: this expression can be simplified. + --> linter-test:4:17 + | + 4 | let a = !true; + | ^^^^^ +Warning [WLINT03711003]: double_negation: this expression can be simplified. + --> linter-test:5:17 + | + 5 | let b = !!false; + | ^^^^^^^ +Warning [WLINT03711003]: nonminimal_boolean: this expression can be simplified. + --> linter-test:5:18 + | + 5 | let b = !!false; + | ^^^^^^ diff --git a/tests/expectations/linter/unnecessary_braces.out b/tests/expectations/linter/unnecessary_braces.out new file mode 100644 index 0000000000..3decdc2644 --- /dev/null +++ b/tests/expectations/linter/unnecessary_braces.out @@ -0,0 +1,39 @@ +Warning [WLINT03711005]: Unnecessary braces around the statements. + --> linter-test:3:30 + | + 3 | transition main() -> u32 {{ + | ^^ + 4 | let a = 4u32; + | ^^^^^^^^^^^^^ + 5 | { + | ^ + 6 | + 7 | } + | ^ + 8 | + 9 | return a; + | ^^^^^^^^^ + 10 | }} + | ^^ + | + = Consider removing the extra braces. +Warning [WLINT03711006]: Empty block statement. + --> linter-test:5:9 + | + 5 | { + | ^ + 6 | + 7 | } + | ^ + | + = Consider removing the block or adding statements to it. +Warning [WLINT03711006]: Empty block statement. + --> linter-test:5:9 + | + 5 | { + | ^ + 6 | + 7 | } + | ^ + | + = Consider removing the block or adding statements to it. diff --git a/tests/expectations/linter/useless_parens.out b/tests/expectations/linter/useless_parens.out new file mode 100644 index 0000000000..30b7080458 --- /dev/null +++ b/tests/expectations/linter/useless_parens.out @@ -0,0 +1,7 @@ +Warning [WLINT03711004]: Unnecessary parentheses around the expression. + --> dep.leo:2:17 + | + 2 | const x: u32 = (4u32); + | ^^^^ + | + = Consider replacing the expression with '4'. diff --git a/tests/expectations/linter/zero_prefixed_literals.out b/tests/expectations/linter/zero_prefixed_literals.out new file mode 100644 index 0000000000..a9f96344f1 --- /dev/null +++ b/tests/expectations/linter/zero_prefixed_literals.out @@ -0,0 +1,10 @@ +Warning [WLINT03711010]: literal has leading zeroes + --> dep.leo:2:16 + | + 2 | const x: u32 = 00123u32; + | ^^^^^^^^ + | + = consider removing the leading zeroes to imporve coding practices + +- 00123 ++ 123 diff --git a/tests/tests/linter/binary.leo b/tests/tests/linter/binary.leo new file mode 100644 index 0000000000..bcf42d0ae8 --- /dev/null +++ b/tests/tests/linter/binary.leo @@ -0,0 +1,36 @@ + +program test.aleo { + transition division_by_zero() -> u32 { + let a: u32 = 5u32 / 0u32; + return a; + } + + transition multiplication_by_one() -> u32 { + let a: u32 = 5u32 * 1u32; + return a; + } + + transition addition_by_zero() -> u32 { + let a: u32 = 5u32 + 0u32; + return a; + } + + transition division_by_one() -> u32 { + let a: u32 = 5u32 / 1u32; + return a; + } + + transition irrefutable_pattern() { + let a: bool = true == true; + let b: bool = true != false; + let c: bool = 4u64 == 4u64; + let d: bool = 4u32 != 3u32; + + assert(a); + assert(c); + assert(b); + assert(d); + + return; + } +} diff --git a/tests/tests/linter/duplicate_imports.leo b/tests/tests/linter/duplicate_imports.leo new file mode 100644 index 0000000000..ba1d7c3694 --- /dev/null +++ b/tests/tests/linter/duplicate_imports.leo @@ -0,0 +1,12 @@ + +import mod.aleo; + +import mod.aleo; + +import mod.aleo; + +program my.aleo { + transition main() { + return; + } +} diff --git a/tests/tests/linter/semantics.leo b/tests/tests/linter/semantics.leo new file mode 100644 index 0000000000..b07241f544 --- /dev/null +++ b/tests/tests/linter/semantics.leo @@ -0,0 +1,17 @@ + +program my.aleo { + transition main() -> u32 { + let a = 4u32; + let b = 4u32; + let c = 4u32; + c = 5u32; + + if true { + a = 3u32; + } else { + a = 6u32; + } + + return a; + } +} diff --git a/tests/tests/linter/unary.leo b/tests/tests/linter/unary.leo new file mode 100644 index 0000000000..ce96fc93bf --- /dev/null +++ b/tests/tests/linter/unary.leo @@ -0,0 +1,10 @@ + +program my.aleo { + transition main() { + let a = !true; + let b = !!false; + assert(a == false); + assert(b == false); + return; + } +} diff --git a/tests/tests/linter/unnecessary_braces.leo b/tests/tests/linter/unnecessary_braces.leo new file mode 100644 index 0000000000..141f61e5ee --- /dev/null +++ b/tests/tests/linter/unnecessary_braces.leo @@ -0,0 +1,11 @@ + +program my.aleo { + transition main() -> u32 {{ + let a = 4u32; + { + + } + + return a; + }} +} diff --git a/tests/tests/linter/useless_parens.leo b/tests/tests/linter/useless_parens.leo new file mode 100644 index 0000000000..989a60dbe0 --- /dev/null +++ b/tests/tests/linter/useless_parens.leo @@ -0,0 +1,3 @@ +// --- Next Module: dep.leo --- // + +const x: u32 = (4u32); diff --git a/tests/tests/linter/zero_prefixed_literals.leo b/tests/tests/linter/zero_prefixed_literals.leo new file mode 100644 index 0000000000..6c1d7db7b2 --- /dev/null +++ b/tests/tests/linter/zero_prefixed_literals.leo @@ -0,0 +1,5 @@ +// --- Next Module: dep.leo --- // + +const x: u32 = 00123u32; + +const y: u64 = 0u64;