From 003423ad217180c26f5586f108becb85001ed298 Mon Sep 17 00:00:00 2001 From: dcrosby Date: Mon, 28 Aug 2023 13:00:17 -0700 Subject: [PATCH] Initial commit of standalone git repo Largely unchanged from fb_bookworm cookbook in https://github.com/facebook/chef-cookbooks/commit/7cbfb039b3e83b4c2426763bf963d5ea8eb19d27 Adjustments are around directory layout and fixing require paths --- .github/workflows/ci.yml | 34 ++ .gitignore | 25 ++ .mdlrc | 1 + .rubocop.yml | 2 + CHANGELOG.md | 4 + CODE_OF_CONDUCT.md | 6 + CONTRIBUTING.md | 39 +++ Gemfile | 6 + LICENSE | 201 ++++++++++++ README.md | 161 +++++++++ RELEASE_PROCESS.md | 21 ++ bin/bookworm | 308 ++++++++++++++++++ fb_bookworm.gemspec | 37 +++ lib/bookworm/configuration.rb | 53 +++ lib/bookworm/crawler.rb | 69 ++++ lib/bookworm/exceptions.rb | 20 ++ lib/bookworm/infer_base_classes.rb | 80 +++++ lib/bookworm/infer_engine.rb | 45 +++ lib/bookworm/keys.rb | 77 +++++ lib/bookworm/knowledge_base.rb | 117 +++++++ lib/bookworm/load_hack.rb | 21 ++ lib/bookworm/report_builder.rb | 93 ++++++ lib/bookworm/reports/AllReferencedRecipes.rb | 35 ++ lib/bookworm/reports/AllRoleDescriptions.rb | 25 ++ lib/bookworm/reports/CookbookDependencyDot.rb | 30 ++ .../reports/DynamicRecipeInclusion.rb | 29 ++ lib/bookworm/reports/LeafCookbooks.rb | 30 ++ .../LibraryDefinedModulesAndClassConstants.rb | 27 ++ .../reports/MissingReferencedRecipes.rb | 61 ++++ lib/bookworm/reports/NoParsedRuby.rb | 46 +++ lib/bookworm/reports/NotReferencedRecipes.rb | 37 +++ .../reports/RecipesAssigningConstants.rb | 27 ++ lib/bookworm/reports/RoleRecipeEntrypoints.rb | 30 ++ lib/bookworm/reports/RoleReferencedRoles.rb | 30 ++ lib/bookworm/rules/ExplicitMetadataDepends.rb | 22 ++ lib/bookworm/rules/IncludeRecipeDynamic.rb | 24 ++ lib/bookworm/rules/IncludeRecipeLiterals.rb | 37 +++ .../rules/LibraryDefinedClassConstants.rb | 22 ++ .../rules/LibraryDefinedModuleConstants.rb | 22 ++ lib/bookworm/rules/NoParsedRuby.rb | 30 ++ .../rules/RecipeConstantAssignments.rb | 22 ++ lib/bookworm/rules/RoleDescription.rb | 22 ++ lib/bookworm/rules/RoleExplicitRoles.rb | 29 ++ lib/bookworm/rules/RoleName.rb | 22 ++ lib/bookworm/rules/RoleRunList.rb | 22 ++ lib/bookworm/rules/RoleRunListRecipes.rb | 29 ++ spec/knowledge_base_spec.rb | 69 ++++ spec/rules/ExplicitMetadataDepends_spec.rb | 30 ++ spec/rules/IncludeRecipeDynamic_spec.rb | 53 +++ spec/rules/IncludeRecipeLiterals_spec.rb | 59 ++++ .../LibraryDefinedClassConstants_spec.rb | 50 +++ .../LibraryDefinedModuleConstants_spec.rb | 50 +++ spec/rules/NoParsedRuby_spec.rb | 36 ++ spec/rules/RecipeConstantAssignments_spec.rb | 41 +++ spec/rules/RoleDescription_spec.rb | 27 ++ spec/rules/RoleExplicitRoles_spec.rb | 33 ++ spec/rules/RoleName_spec.rb | 27 ++ spec/rules/RoleRunListRecipes_spec.rb | 34 ++ spec/rules/RoleRunList_spec.rb | 33 ++ spec/rules/helper.rb | 32 ++ spec/spec_helper.rb | 23 ++ 61 files changed, 2727 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .mdlrc create mode 100644 .rubocop.yml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Gemfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 RELEASE_PROCESS.md create mode 100755 bin/bookworm create mode 100644 fb_bookworm.gemspec create mode 100644 lib/bookworm/configuration.rb create mode 100644 lib/bookworm/crawler.rb create mode 100644 lib/bookworm/exceptions.rb create mode 100644 lib/bookworm/infer_base_classes.rb create mode 100644 lib/bookworm/infer_engine.rb create mode 100644 lib/bookworm/keys.rb create mode 100644 lib/bookworm/knowledge_base.rb create mode 100644 lib/bookworm/load_hack.rb create mode 100644 lib/bookworm/report_builder.rb create mode 100644 lib/bookworm/reports/AllReferencedRecipes.rb create mode 100644 lib/bookworm/reports/AllRoleDescriptions.rb create mode 100644 lib/bookworm/reports/CookbookDependencyDot.rb create mode 100644 lib/bookworm/reports/DynamicRecipeInclusion.rb create mode 100644 lib/bookworm/reports/LeafCookbooks.rb create mode 100644 lib/bookworm/reports/LibraryDefinedModulesAndClassConstants.rb create mode 100644 lib/bookworm/reports/MissingReferencedRecipes.rb create mode 100644 lib/bookworm/reports/NoParsedRuby.rb create mode 100644 lib/bookworm/reports/NotReferencedRecipes.rb create mode 100644 lib/bookworm/reports/RecipesAssigningConstants.rb create mode 100644 lib/bookworm/reports/RoleRecipeEntrypoints.rb create mode 100644 lib/bookworm/reports/RoleReferencedRoles.rb create mode 100644 lib/bookworm/rules/ExplicitMetadataDepends.rb create mode 100644 lib/bookworm/rules/IncludeRecipeDynamic.rb create mode 100644 lib/bookworm/rules/IncludeRecipeLiterals.rb create mode 100644 lib/bookworm/rules/LibraryDefinedClassConstants.rb create mode 100644 lib/bookworm/rules/LibraryDefinedModuleConstants.rb create mode 100644 lib/bookworm/rules/NoParsedRuby.rb create mode 100644 lib/bookworm/rules/RecipeConstantAssignments.rb create mode 100644 lib/bookworm/rules/RoleDescription.rb create mode 100644 lib/bookworm/rules/RoleExplicitRoles.rb create mode 100644 lib/bookworm/rules/RoleName.rb create mode 100644 lib/bookworm/rules/RoleRunList.rb create mode 100644 lib/bookworm/rules/RoleRunListRecipes.rb create mode 100644 spec/knowledge_base_spec.rb create mode 100644 spec/rules/ExplicitMetadataDepends_spec.rb create mode 100644 spec/rules/IncludeRecipeDynamic_spec.rb create mode 100644 spec/rules/IncludeRecipeLiterals_spec.rb create mode 100644 spec/rules/LibraryDefinedClassConstants_spec.rb create mode 100644 spec/rules/LibraryDefinedModuleConstants_spec.rb create mode 100644 spec/rules/NoParsedRuby_spec.rb create mode 100644 spec/rules/RecipeConstantAssignments_spec.rb create mode 100644 spec/rules/RoleDescription_spec.rb create mode 100644 spec/rules/RoleExplicitRoles_spec.rb create mode 100644 spec/rules/RoleName_spec.rb create mode 100644 spec/rules/RoleRunListRecipes_spec.rb create mode 100644 spec/rules/RoleRunList_spec.rb create mode 100644 spec/rules/helper.rb create mode 100644 spec/spec_helper.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ecd2cda --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: Continuous Integration +on: + push: + branches: [main] + pull_request: +jobs: + ruby: + strategy: + fail-fast: false + matrix: + ruby: [2.5, 2.6, 2.7] + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + - name: Install dependencies + run: bundle install + - name: Run rspec + run: bundle exec rspec + - name: Run rubocop + run: bundle exec rubocop --display-cop-names + markdown: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Lint Markdown + uses: actionshub/markdownlint@1.2.0 + - name: Check links + uses: gaurav-nelson/github-action-markdown-link-check@v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f8f627 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp +*.bundle +*.so +*.o +*.a +mkmf.log +bin/ +sbin/ +coverage diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 0000000..73777ae --- /dev/null +++ b/.mdlrc @@ -0,0 +1 @@ +rules "~MD003", "~MD007", "~MD013", "~MD022", "~MD032" diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..9628f96 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,2 @@ +inherit_from: + - https://raw.githubusercontent.com/facebook/chef-cookbooks/main/.rubocop.yml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7ffa65b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## 0.0.1 +* Initial standalone gem release diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..abca6fd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,6 @@ +# Code of Conduct + +Meta has adopted a Code of Conduct that we expect project participants to +adhere to. +Please read the [full text](https://opensource.fb.com/code-of-conduct/) +so that you can understand what actions will and will not be tolerated. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4c92bf2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Contributing to Bookworm +We want to make contributing to this project as easy and transparent as +possible. + +## Our Development Process +All changes to Bookworm are developed on this GitHub repo. Changes +committed are then rolled out internally before a formal release is done on +RubyGems. + +## Pull Requests +We actively welcome your pull requests. + +1. Fork the repo and create your branch from `main`. +1. If you've added code that should be tested, add tests. +1. If you've changed APIs, update the documentation. +1. Ensure the test suite passes. +1. Make sure your code lints. +1. If you haven't already, complete the Contributor License Agreement ("CLA"). + +## Contributor License Agreement ("CLA") +In order to accept your pull request, we need you to submit a CLA. You only need +to do this once to work on any of Facebook's open source projects. + +Complete your CLA here: + +## Issues +We use GitHub issues to track public bugs. Please ensure your description is +clear and has sufficient instructions to be able to reproduce the issue. + +Meta has a [bounty program](https://www.facebook.com/whitehat/) for the safe +disclosure of security bugs. In those cases, please go through the process +outlined on that page and do not file a public issue. + +## Coding Style +We use Rubocop, see the .rubocop.yml + +## License +By contributing to Bookworm, you agree that your contributions will be +licensed under the LICENSE file in the root directory of this source tree. diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..de2c506 --- /dev/null +++ b/Gemfile @@ -0,0 +1,6 @@ +source 'https://rubygems.org' + +gemspec + +gem 'rspec', '= 3.11' +gem 'rubocop', '= 1.25.1' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..11069ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7032d5 --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +fb_bookworm +=========== + +[![Continuous Integration](https://github.com/facebook/bookworm/workflows/Continuous%20Integration/badge.svg?event=push)](https://github.com/facebook/bookworm/actions?query=workflow%3A%22Continuous+Integration%22) + +Bookworm is a program that gleans context from a Chef/Ruby codebase, which +recognizes that Ruby source files in different directories have different +semantic meaning to a larger program (ie Chef) + +It currently runs on top of the Chef Workstation Ruby, although there is +nothing preventing running bookworm on vanilla Ruby using bundler, etc. + +Usage +----- + +Bookworm is designed to be installed via Chef cookbook as well as running +directly from the cookbook. This assumes you have at least [Chef Workstation +20](https://www.chef.io/downloads/tools/workstation) installed. If you run this +cookbook in a Chef run, it will also install Bookworm into +`/usr/local/lib/bookworm` (with a shell script launcher at +`/usr/local/bin/bookworm`) + +``` +# If you're running directly from the cookbook +cd files/default/bookworm + +# Get the program flags +$ ./bookworm.rb -h +Usage: bookworm.rb [options] + --report CLASS Give the (class) name of the report you'd like + --list-reports Get the (class) names of available reports + --list-rules Get the (class) names of available inference rules + +Debugging options: + --irb-config-step Open IRB REPL after loading configuration + --irb-crawl-step Open IRB REPL after crawler has run + --irb-infer-step Open IRB REPL after inference has run + --irb-report-step Open IRB REPL after report is generated + +# Get a list of reports that can be run +$ bookworm --list-reports +AllReferencedRecipes Determines all recipes that are directly referenced by all roles +AllRoleDescriptions Basic report to show extraction of description from roles +... + +# Run a report +$ bookworm --report AllRoleDescriptions +``` + +Configuring Bookworm +-------------------- + +Bookworm currently checks 2 different places for configuration, +`~/.bookworm.yml` and `/usr/local/etc/bookworm/configuration.rb` + +### ~/.bookworm.yml + +A typical bookworm YAML configuration would look something like this: + +```yaml +# A basic chef layout +source_dirs: + cookbook_dirs: + - /absolute/path/to/cookbooks + - /absolute/path/to/some/other/cookbooks + role_dirs: + - /absolute/path/to/role/files +debug: false # Additional debug information +``` + +### /usr/local/etc/bookworm/configuration.rb + +If you need to programmatically determine what your `source_dirs` are (ie if +you don't know what directories Bookworm will be running on ahead of time), you +can use `/usr/local/etc/bookworm/configuration.rb` to set `DEFAULT_SOURCE_DIRS` +and `DEFAULT_DEBUG`. This might be overkill for your needs, and +`~/.bookworm.yml` is probably your best bet. + +Glossary +-------- + +### Bookworm key + +A bookworm key is a type of file, and all the information that should be +necessary to handle that file throughout the Bookworm pipeline. By +encapsulating the "what" and the "how" of a file here, adding new types of +files to Bookworm shouldn't involve more than adding a new key. + +Of note, a 'metakey' isn't a file - it could be a concept or a group of files. +This is particularly useful with Chef code, since the notion of a 'cookbook' is +several things, and rather than doing backflips trying to match information to +specific files within a cookbook directory, it's much simpler to say "this +cookbook provides this resource/class/attribute." + +### Crawler + +The crawler is what transforms whatever the key finds into a navigable Ruby +representation of the file (for Ruby files, this would be an AST representation +of the source file as generated by the rubocop-ast gem). + +### KnowledgeBase + +The KnowledgeBase is the singleton object that holds all Ruby representations +of the crawled files, as well as all information derived by the Bookworm rules. + +### Rule + +A 'rule' in Bookworm is an auto-generated class that takes the crawled +representation of a file and extracts specific information about it. If you +want to know about 3 unrelated patterns in a source file, consider using (or +writing, if they don't exist yet) 3 rules. Small rules encourage re-use in +reports, and since rules can reference information about a file derived from +*other* rules, this is a good place to do any heavy lifting when figuring out +what's going on with your keys. + +### InferEngine + +The InferEngine runs the necessary rules against each key that was crawled. + +### Reports + +The report takes the information that you've extracted from the codebase, and +pulls it together into a human or machine-readable representation. While this +is a good place to store specific logic for representation, it doesn't hurt to +ask 'how do I delegate the hard work to rules' - think of reports as the glue +that pulls together a bunch of derived facts into something nice that you can +hand to your boss ;-) + +Guiding design principles +------------------------- + +- Easy and fast iteration + - Writing a Bookworm rule or report should be as easy as grabbing bash and grep + - Profiling and debugging hooks should be in strategic spots to make it easy to debug +- Each new rule or report unlocks the potential for new discoveries + - It should be easy to use existing work, so that building reports is just a matter of grabbing existing rules (or other reports) +- Zero-cost additions, so you only analyze/execute the report (and supporting + rules) that are needed + - A slow rule shouldn't matter unless you're actually using it + - Accept that copy-paste is going to happen with rules and reports, and make that simple. + +Implementation notes - Why Rubocop for AST generation +----------------------------------------------------- + +Because we wanted to use something that was already in Chef Workstation, the +two choices were Ripper or Parser a la RuboCop (ruby_parser uses racc which has +a C extension, but no sexp pattern matcher that I know of). + +Ripper is fast, but the sexp output is kind of nasty, and cleaning that up +could be a big timesuck. Since the larger Ruby/Chef community has a bit more +familiarity with Parser/RuboCop's node pattern matching, it'd be better to stay +with that for now (there's no reason this couldn't be migrated later with +helpers to translate patterns). Work could also be done to speed up RuboCop +(ractor and async support could go a long way here). +This repo contains attribute-driven-API cookbooks maintained by Facebook. It's +a large chunk of what we refer to as our "core cookbooks." + +License +------- + +See the LICENSE file in this directory diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md new file mode 100644 index 0000000..cbef014 --- /dev/null +++ b/RELEASE_PROCESS.md @@ -0,0 +1,21 @@ +Release process +=============== + +On your checkout +---------------- + +* Update the version number and changelog, commit locally, push + vi CHANGELOG.md + vi fb_bookworm.gemspec + # create a pull request and merge it + +On a checkout of the **UPSTREAM** repo +-------------------------------------- + +* Add and push a tag: + git tag -a v0.0.X -m 'version 0.0.X' + git push origin --tags + +* Build and push a release: + gem build fb_bookworm.gemspec + gem push fb_bookworm-0.0.X.gem diff --git a/bin/bookworm b/bin/bookworm new file mode 100755 index 0000000..257533c --- /dev/null +++ b/bin/bookworm @@ -0,0 +1,308 @@ +#!/opt/chef-workstation/embedded/bin/ruby +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'optparse' +module Bookworm + class CLIParser + def initialize + parser = ::OptionParser.new + + parser.banner = 'Usage: bookworm.rb [options]' + + # TODO(dcrosby) explicitly output to stdout? + # parser.on( + # '--output TYPE', + # '(STUB) Configure output type for report. Options: plain (default), JSON', + # ) + + parser.on( + '--report CLASS', + "Give the (class) name of the report you'd like", + ) + + parser.on( + '--list-reports', + 'Get the (class) names of available reports', + ) + + parser.on( + '--list-rules', + 'Get the (class) names of available inference rules', + ) + + parser.separator '' + parser.separator 'Debugging options:' + + # TODO(dcrosby) add verbose mode + # parser.on( + # '--verbose', + # 'Enable verbose mode', + # ) + + # TODO(dcrosby) get ruby-prof working + # parser.on( + # '--profiler', + # '(WIP) Enable profiler for performance debugging', + # ) + + parser.on( + '--irb-config-step', + 'Open IRB REPL after loading configuration', + ) + + parser.on( + '--irb-crawl-step', + 'Open IRB REPL after crawler has run', + ) + + parser.on( + '--irb-infer-step', + 'Open IRB REPL after inference has run', + ) + + parser.on( + '--irb-report-step', + 'Open IRB REPL after report is generated', + ) + + @parser = parser + end + + def help + @parser.help + end + + def parse + options = {} + @parser.parse(ARGV, :into => options) + options + end + end +end +parser = Bookworm::CLIParser.new +options = parser.parse +# TODO(dcrosby) get ruby-prof working +# if options[:profiler] +# require 'ruby-prof' +# RubyProf.start +# end + +# We require the libraries *after* the profiler has a chance to start, +# also means faster `bookworm -h` response +require 'set' +require 'bookworm/exceptions' +require 'bookworm/keys' +require 'bookworm/configuration' +require 'bookworm/crawler' +require 'bookworm/knowledge_base' +require 'bookworm/infer_engine' +require 'bookworm/report_builder' + +module Bookworm + class ClassLoadError < RuntimeError; end + + # Class to hold state of a Bookworm run + class Run + attr_reader :cli_help_message, :config, :report_src_dirs, :rule_src_dirs, :action, :irb_breakpoints, :report_name + + def initialize(cli_options, cli_help_message) + @cli_help_message = cli_help_message + validate_cli_args(cli_options) + set_irb_breakpoints(cli_options) + generate_config + validate_config_file + load_src_dirs + determine_action(cli_options) + binding.irb if irb_breakpoint?('config') # rubocop:disable Lint/Debugger + end + + def set_irb_breakpoints(options) + @irb_breakpoints = [] + %w{config crawl infer report}.each do |bp| + @irb_breakpoints << bp if options["irb-#{bp}-step".to_sym] + end + end + + def irb_breakpoint?(str) + @irb_breakpoints.include?(str) + end + + def do_action + case @action + when :"list-reports" + list_reports + when :"list-rules" + list_rules + when :report + generate_report + end + end + + def determine_action(options) + [:"list-reports", :"list-rules", :report].each do |a| + if options[a] + if @action + cli_fail 'Multiple actions specified, check your arguments' + else + @action = a + end + end + end + @report_name = options[:report] + end + + def generate_config + # TODO(dcrosby) read CLI for config file path + @config = Bookworm::Configuration.new + end + + def cli_fail(msg) + puts "#{msg}\n\n#{@cli_help_message}" + exit(false) + end + + def validate_cli_args(options) + unless options[:"list-reports"] || options[:"list-rules"] + unless options[:report] + cli_fail 'No report name given, take a look at bookworm --list-reports' + end + end + end + + def validate_config_file + if @config.source_dirs.nil? || @config.source_dirs.empty? + fail 'configuration source_dirs cannot be empty' + end + end + + def load_src_dirs + require 'bookworm/load_hack' + @report_src_dirs = [::Bookworm::BUILTIN_REPORTS_DIR] + if Dir.exist? "#{@config.system_contrib_dir}/reports" + @report_src_dirs.append "#{@config.system_contrib_dir}/reports" + end + @rule_src_dirs = [::Bookworm::BUILTIN_RULES_DIR] + if Dir.exist? "#{@config.system_contrib_dir}/rules/" + @rule_src_dirs.append "#{@config.system_contrib_dir}/rules/" + end + end + + def list_reports + @report_src_dirs.each do |d| + Bookworm.load_reports_dir d + end + + puts Bookworm::Reports.constants.map { |x| + "#{x}\t#{Module.const_get("Bookworm::Reports::#{x}")&.description}" + }.sort.join("\n") + end + + def list_rules + @rule_src_dirs.each do |d| + Bookworm.load_rules_dir d + end + puts Bookworm::InferRules.constants.map { |x| + "#{x}\t#{Module.const_get("Bookworm::InferRules::#{x}")&.description}" + }.sort.join("\n") + end + + def generate_report + load_classes_for_report + crawl_source + make_inferences + build_report + end + + def load_classes_for_report + @report_src_dirs.each do |d| + + Bookworm.load_report_class @report_name, :dir => d + break + rescue Bookworm::ClassLoadError + # puts "Unable to load report #{report_name}, take a look at bookworm --list-reports\n\n" + + end + unless Bookworm::Reports.const_defined?(@report_name.to_sym) + cli_fail "Unable to load report #{@report_name}, take a look at bookworm --list-reports" + end + + # To keep processing to only what is needed, the rules are specified within + # the report. From those rules, we gather the keys that actually need to be + # crawled (instead of crawling everything) + # TODO(dcrosby) recursively check rules for dependency keys + @rules = Bookworm.get_report_rules(@report_name) + @rules.each do |rule| + @rule_src_dirs.each do |d| + + Bookworm.load_rule_class rule, :dir => d + break + rescue Bookworm::ClassLoadError + # puts "Unable to load rule #{rule}, take a look at bookworm --list-rules\n\n" + + end + unless Bookworm::InferRules.const_defined?(rule.to_sym) + cli_fail "Unable to load rule #{rule}, take a look at bookworm --list-rules" + end + end + end + + def crawl_source + # Determine necessary keys to crawl + keys = @rules.map { |r| Module.const_get("Bookworm::InferRules::#{r}")&.keys }.flatten.uniq + + # The crawler determines the files that need to be processed + # It currently converts Ruby source files to AST/objects (that may change) + processed_files = Bookworm::Crawler.new(config, :keys => keys).processed_files + + # The knowledge base is what we know about the files (AST, paths, + # digested information from inference rules, etc) + @knowledge_base = Bookworm::KnowledgeBase.new(processed_files) + + binding.irb if irb_breakpoint?('crawl') # rubocop:disable Lint/Debugger + end + + def make_inferences + # InferEngine takes the crawler output in the knowledge base and runs a series + # of Infer rules against the source AST (and more) to build a knowledge base + # around the source + # It runs classes within the Bookworm::InferRules module namespace + engine = Bookworm::InferEngine.new(@knowledge_base, @rules) + @knowledge_base = engine.knowledge_base + + binding.irb if irb_breakpoint?('infer') # rubocop:disable Lint/Debugger + end + + def build_report + # The ReportBuilder takes a knowledge base and generates a report + # with each class in the Bookworm::Reports module namespace + Bookworm::ReportBuilder.new(@knowledge_base, @report_name) + + binding.irb if irb_breakpoint?('report') # rubocop:disable Lint/Debugger + end + end +end + +if __FILE__ == $PROGRAM_NAME || $PROGRAM_NAME == './bin/bookworm' + run = Bookworm::Run.new(options, parser.help) + run.do_action +end + +# TODO(dcrosby) get ruby-prof working +# if options[:profiler] +# result = RubyProf.stop +# printer = RubyProf::FlatPrinter.new(result) +# printer.print($stdout) +# end diff --git a/fb_bookworm.gemspec b/fb_bookworm.gemspec new file mode 100644 index 0000000..48ac11a --- /dev/null +++ b/fb_bookworm.gemspec @@ -0,0 +1,37 @@ +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 + +# Copyright 2013-present Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +Gem::Specification.new do |s| + s.name = 'fb_bookworm' + s.version = '0.0.1' + s.summary = 'fb_bookworm' + s.description = 'Program to build context around Chef cookbook code' + s.license = 'Apache-2.0' + s.authors = ['David Crosby'] + s.homepage = 'https://github.com/facebook/bookworm' + s.platform = Gem::Platform::RUBY + + s.extra_rdoc_files = %w{README.md LICENSE} + + s.files = %w{README.md LICENSE} + + Dir.glob('lib/**/*', File::FNM_DOTMATCH).reject { |f| File.directory?(f) } + s.bindir = 'exe' + s.executables = s.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + s.require_paths = ['lib'] + + s.required_ruby_version = '>= 2.5.0' + s.add_dependency 'rubocop', '>= 1.25' +end diff --git a/lib/bookworm/configuration.rb b/lib/bookworm/configuration.rb new file mode 100644 index 0000000..6ac6350 --- /dev/null +++ b/lib/bookworm/configuration.rb @@ -0,0 +1,53 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +module Bookworm + class Configuration + attr :source_dirs, :debug + attr_reader :system_contrib_dir + + # Allow for programmatic configuration override + SYSTEM_CONFIGURATION_RUBY_FILE = '/usr/local/etc/bookworm/configuration.rb'.freeze + SYSTEM_CONTRIB_DIR = '/usr/local/etc/bookworm/contrib'.freeze + + def initialize + begin + load SYSTEM_CONFIGURATION_RUBY_FILE + rescue LoadError + # puts "No configuration found at #{SYSTEM_CONFIGURATION_RUBY_FILE}" + config = {} + end + begin + config = YAML.load_file "#{Dir.home}/.bookworm.yml" + rescue StandardError + config = {} + end + + @system_contrib_dir = SYSTEM_CONTRIB_DIR + if Bookworm::Configuration.const_defined?(:DEFAULT_SOURCE_DIRS) + @source_dirs ||= DEFAULT_SOURCE_DIRS + end + if Bookworm::Configuration.const_defined?(:DEFAULT_DEBUG) + @debug ||= DEFAULT_DEBUG + end + + @system_contrib_dir = config['system_contrib_dir'] if config['system_contrib_dir'] + @source_dirs = config['source_dirs'] if config['source_dirs'] + @debug = config['debug'] if config['debug'] + + @source_dirs ||= [] + end + end +end diff --git a/lib/bookworm/crawler.rb b/lib/bookworm/crawler.rb new file mode 100644 index 0000000..34c6171 --- /dev/null +++ b/lib/bookworm/crawler.rb @@ -0,0 +1,69 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +require 'rubocop' +require 'parser/current' + +# TODO(dcrosby) Bookworm key should determine which AST parser is chosen. This +# would allow multiple AST parsers (ie ripper) and a way of ingesting non-Ruby +# files like JSON/YAML + +module Bookworm + class Crawler + attr_reader :processed_files + + def initialize(config, keys: []) + @config = config + @intake_queue = {} + @processed_files = {} + + # TODO(dcrosby) add messages to verbose mode + keys.each do |key| + generate_file_queue(key) + process_paths(key) + end + end + + private + + def generate_file_queue(key) + v = BOOKWORM_KEYS[key] + @intake_queue[key] = @config.source_dirs[v['source_dirs']]. + map { |d| Dir.glob("#{d}/#{v['glob_pattern']}") }.flatten + end + + def process_paths(key) + queue = @intake_queue[key] + processed_files = {} + until queue.empty? + path = queue.pop + processed_files[path] = generate_ast(File.read(path)) + end + @processed_files[key] = processed_files + end + + # In order to keep rules from barfing on a nil value (when no AST is + # generated at all from eg. an empty source code file), we supply a + # single node called bookworm_found_nil. It's a magic value that is + # 'unique enough' for our purposes + EMPTY_RUBOCOP_AST = ::RuboCop::AST::Node.new('bookworm_found_nil').freeze + def generate_rubocop_ast(code) + ::RuboCop::ProcessedSource.new(code, RUBY_VERSION.to_f)&.ast || + EMPTY_RUBOCOP_AST + end + + alias generate_ast generate_rubocop_ast + end +end diff --git a/lib/bookworm/exceptions.rb b/lib/bookworm/exceptions.rb new file mode 100644 index 0000000..d097af4 --- /dev/null +++ b/lib/bookworm/exceptions.rb @@ -0,0 +1,20 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +module Bookworm + # ClassLoadError is where Bookworm tries to autogenerate a class from a file + # and cannot + class ClassLoadError < RuntimeError; end +end diff --git a/lib/bookworm/infer_base_classes.rb b/lib/bookworm/infer_base_classes.rb new file mode 100644 index 0000000..ff8ea5e --- /dev/null +++ b/lib/bookworm/infer_base_classes.rb @@ -0,0 +1,80 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +require 'rubocop' +require 'pathname' + +module Bookworm + class InferRule + class << self + { + 'description' => '', + 'keys' => [], + }.each do |attribute, default_value| + instance_variable_set("@#{attribute}".to_sym, default_value) + define_method(attribute.to_sym) do |val = nil| + instance_variable_set("@#{attribute}", val) unless val.nil? + instance_variable_get("@#{attribute}") + end + end + end + + extend RuboCop::NodePattern::Macros + def initialize(metadata) + @metadata = metadata + output + end + + def to_a + [] + end + + def to_h + {} + end + + def default_output + :to_a + end + + def output + send(default_output) + end + end + + # Initializing constant for Bookworm::InferRules + module InferRules; end + + def self.load_rule_class(name, dir: '') + f = File.read "#{dir}/#{name.to_sym}.rb" + ::Bookworm::InferRules.const_set(name.to_sym, ::Class.new(::Bookworm::InferRule)) + ::Bookworm::InferRules.const_get(name.to_sym).class_eval(f) + rescue StandardError + raise Bookworm::ClassLoadError + end + + def self.load_rules_dir(dir) + files = Dir.glob("#{dir}/*.rb") + files.each do |f| + name = Pathname(f).basename.to_s.gsub('.rb', '') + begin + Bookworm.load_rule_class name, :dir => dir + rescue Bookworm::ClassLoadError + puts "Unable to load rule #{f}" + exit(false) + end + end + end +end diff --git a/lib/bookworm/infer_engine.rb b/lib/bookworm/infer_engine.rb new file mode 100644 index 0000000..f094b4f --- /dev/null +++ b/lib/bookworm/infer_engine.rb @@ -0,0 +1,45 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +require 'bookworm/knowledge_base' +require 'bookworm/infer_base_classes' + +module Bookworm + # The InferEngine class takes a KnowledgeBase object, and then runs the given + # rules against the files within each bookworm key in the KnowledgeBase that + # the rule uses. + class InferEngine + def initialize(knowledge_base, rules = []) + @kb = knowledge_base + + rules.each do |rule| + process_rule(rule) + end + end + + def process_rule(rule) + klass = Bookworm::InferRules.const_get(rule) + klass.keys.each do |key| + @kb[key].each do |name, metadata| + @kb[key][name][rule] = klass.new(metadata).output + end + end + end + + def knowledge_base + @kb + end + end +end diff --git a/lib/bookworm/keys.rb b/lib/bookworm/keys.rb new file mode 100644 index 0000000..248fd1e --- /dev/null +++ b/lib/bookworm/keys.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +module Bookworm + # These keys are how we generalize and generate a lot of code within Bookworm + # + # VALUES + # metakey: A metakey isn't crawled, but is useful as a place to store information + # plural: The pluralized form of the key - typically used in method creation + # source_dirs: Specify which array of source directories to crawl + # glob_pattern: Specify the glob pattern for which files to crawl in source directories + # dont_init_kb_key: Don't initialize the keys on knowledge base creation + # determine_cookbook_name: Determines the cookbook name from the given path + # path_name_regex: A regex with a capture to determine the prettified name of the file + BOOKWORM_KEYS = { + 'cookbook' => { + 'metakey' => true, + 'dont_init_kb_key' => true, + }, + 'role' => { + 'source_dirs' => 'role_dirs', + 'glob_pattern' => '*.rb', + 'path_name_regex' => '([\w-]+)\.rb', + }, + 'metadatarb' => { + 'glob_pattern' => '*/metadata.rb', + 'determine_cookbook_name' => true, + 'path_name_regex' => '(metadata\.rb)', + }, + 'recipe' => { + 'determine_cookbook_name' => true, + 'path_name_regex' => 'recipes/(.*)\.rb', + }, + 'attribute' => { + 'determine_cookbook_name' => true, + 'path_name_regex' => 'attributes/(.*)\.rb', + }, + 'library' => { + 'plural' => 'libraries', # <-- the troublemaker that prompted keys first ;-) + 'determine_cookbook_name' => true, + 'path_name_regex' => 'libraries\/(.*)\.rb', + }, + 'resource' => { + 'determine_cookbook_name' => true, + 'path_name_regex' => 'resources/(.*)\.rb', + }, + 'provider' => { + 'determine_cookbook_name' => true, + 'path_name_regex' => 'providers/(.*)\.rb', + }, + }.freeze + + # Set defaults + BOOKWORM_KEYS.each do |k, v| + BOOKWORM_KEYS[k]['determine_cookbook_name'] ||= false + BOOKWORM_KEYS[k]['plural'] ||= "#{k}s" + BOOKWORM_KEYS[k]['source_dirs'] ||= 'cookbook_dirs' + BOOKWORM_KEYS[k]['glob_pattern'] ||= "*/#{v['plural']}/*.rb" + end + + BOOKWORM_KEYS.freeze +end diff --git a/lib/bookworm/knowledge_base.rb b/lib/bookworm/knowledge_base.rb new file mode 100644 index 0000000..c0511ef --- /dev/null +++ b/lib/bookworm/knowledge_base.rb @@ -0,0 +1,117 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +require 'pathname' +module Bookworm + module KnowledgeBaseBackends + # The SimpleHash backend stores the KnowledgeBase information as ... a + # simple hash. No concurrency guarantees, caching, etc. Each bookworm run + # will run all the rules, every time. + module SimpleHash + def init_hooks + @kb_internal_hash = {} + end + + def [](key) + @kb_internal_hash[key] + end + + def []=(key, value) + @kb_internal_hash[key] = value + end + end + end + + # The KnowledgeBase is a backend-agnostic way of storing and querying + # information that's generated about the files via InferRules. + # + # We provide indirect access to the information stored by the + # knowledge base, so that we'll soon be able to leverage different + # backends (for concurrent writes, caching rule output across runs, etc). + class KnowledgeBase + def initialize(opts) + extend Bookworm::KnowledgeBaseBackends::SimpleHash + + init_hooks + + # TODO: Only initialize keys required by rules + BOOKWORM_KEYS.each do |k, v| + unless v['dont_init_kb_key'] + if v['determine_cookbook_name'] + init_key_with_cookbook_name(k, opts[k] || []) + else + init_key(k, opts[k] || []) + end + end + create_pluralized_getter(k) + end + end + + def backend_missing + fail 'Need to specify a backend for KnowledgeBase class' + end + + def [](_key) + backend_missing + end + + def []=(_key, _value) + backend_missing + end + + def init_hooks + # This is optional for KnowledgeBaseBackends + end + + private + + # This creates a method based off the (pluralized) Bookworm key. + # Syntactical sugar that can make some rules/reports easier to read. + def create_pluralized_getter(key) + define_singleton_method(BOOKWORM_KEYS[key]['plural'].to_sym) do + self[key] + end + end + + def init_key(key, files) + self[key] = {} + path_name_regex = BOOKWORM_KEYS[key]['path_name_regex'] + files.each do |path, ast| + m = path.match(/#{path_name_regex}/) + file_name = m[1] + self[key][file_name] = { 'path' => path, 'ast' => ast } + end + end + + # The difference between this method and init_key is that it: + # 1. initializes cookbook metakey if it doesn't already exist + # 2. instead of using the filename for file key, uses COOKBOOK::FILENAME + # where FILENAME has the '.rb' suffix stripped (making it similar to the + # include_recipe calling conventions in Chef + def init_key_with_cookbook_name(key, files) + self[key] = {} + self['cookbook'] ||= {} + path_name_regex = BOOKWORM_KEYS[key]['path_name_regex'] + files.each do |path, ast| + m = path.match(%r{/?([\w-]+)/#{path_name_regex}}) + cookbook_name = m[1] + file_name = m[2] + self['cookbook'][cookbook_name] ||= {} + self[key]["#{cookbook_name}::#{file_name}"] = + { 'path' => path, 'cookbook' => cookbook_name, 'ast' => ast } + end + end + end +end diff --git a/lib/bookworm/load_hack.rb b/lib/bookworm/load_hack.rb new file mode 100644 index 0000000..8ed1117 --- /dev/null +++ b/lib/bookworm/load_hack.rb @@ -0,0 +1,21 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# TODO: This is gross, and I seem to recall there was some way to get a gem's +# library directory, but this should work for now. +module Bookworm + BUILTIN_REPORTS_DIR = "#{__dir__}/reports/".freeze + BUILTIN_RULES_DIR = "#{__dir__}/rules/".freeze +end diff --git a/lib/bookworm/report_builder.rb b/lib/bookworm/report_builder.rb new file mode 100644 index 0000000..20d7e15 --- /dev/null +++ b/lib/bookworm/report_builder.rb @@ -0,0 +1,93 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +require 'json' +require 'pathname' + +module Bookworm + class BaseReport + class << self + { + 'description' => '', + 'needs_rules' => [], + }.each do |attribute, default_value| + instance_variable_set("@#{attribute}".to_sym, default_value) + define_method(attribute.to_sym) do |val = nil| + instance_variable_set("@#{attribute}", val) unless val.nil? + instance_variable_get("@#{attribute}") + end + end + end + + def initialize(knowledge_base) + @kb = knowledge_base + output + end + + def to_plain + '' + end + + def to_json(*_args) + JSON.dump({}) + end + + def default_output + :to_plain + end + + def output + send(default_output) + end + end + + # Initialize constant for Bookworm::Reports + module Reports; end + + class ReportBuilder + def initialize(knowledge_base, report_name) + klass = Module.const_get("Bookworm::Reports::#{report_name}") + output = klass.new(knowledge_base).output + + puts output + end + end + + def self.get_report_rules(report_name) + klass = Module.const_get("Bookworm::Reports::#{report_name}") + klass.needs_rules + end + + def self.load_report_class(name, dir: '') + f = File.read "#{dir}/#{name.to_sym}.rb" + ::Bookworm::Reports.const_set(name.to_sym, ::Class.new(::Bookworm::BaseReport)) + ::Bookworm::Reports.const_get(name.to_sym).class_eval(f) + rescue StandardError + raise Bookworm::ClassLoadError + end + + def self.load_reports_dir(dir) + files = Dir.glob("#{dir}/*.rb") + files.each do |f| + name = Pathname(f).basename.to_s.gsub('.rb', '') + begin + Bookworm.load_report_class name, :dir => dir + rescue Bookworm::ClassLoadError + puts "Unable to load report #{f}" + exit(false) + end + end + end +end diff --git a/lib/bookworm/reports/AllReferencedRecipes.rb b/lib/bookworm/reports/AllReferencedRecipes.rb new file mode 100644 index 0000000..6862443 --- /dev/null +++ b/lib/bookworm/reports/AllReferencedRecipes.rb @@ -0,0 +1,35 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Determines all recipes that are directly referenced by all roles' +needs_rules ['RoleRunListRecipes', 'IncludeRecipeLiterals'] + +def to_a + buffer = Set.new + @kb.roles.each do |_, metadata| + metadata['RoleRunListRecipes'].each do |role| + buffer << role + end + end + @kb.recipes.each do |_, metadata| + metadata['IncludeRecipeLiterals'].each do |recipe| + buffer << recipe + end + end + buffer.sort.to_a +end + +def output + to_a +end diff --git a/lib/bookworm/reports/AllRoleDescriptions.rb b/lib/bookworm/reports/AllRoleDescriptions.rb new file mode 100644 index 0000000..39c7d9b --- /dev/null +++ b/lib/bookworm/reports/AllRoleDescriptions.rb @@ -0,0 +1,25 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Basic report to show extraction of description from roles' +needs_rules ['RoleName', 'RoleDescription'] + +def to_plain + buffer = '' + @kb.roles.sort_by { |k, _| k }.each do |name, metadata| + # buffer << name + buffer << "role: #{name} desc: #{metadata['RoleDescription']}\n" + end + buffer +end diff --git a/lib/bookworm/reports/CookbookDependencyDot.rb b/lib/bookworm/reports/CookbookDependencyDot.rb new file mode 100644 index 0000000..a048b3f --- /dev/null +++ b/lib/bookworm/reports/CookbookDependencyDot.rb @@ -0,0 +1,30 @@ +# Copyright (c) 2023-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Determine cookbook dependencies from cookbook metadata.rb, output to Dot language' +needs_rules ['ExplicitMetadataDepends'] + +def to_s + cookbook_deps = [] + @kb.metadatarbs.each do |x, metadata| + metadata['ExplicitMetadataDepends'].each do |cb| + cookbook_deps << [x.gsub(/:.*/, ''), cb] + end + end + "digraph deps {\n#{cookbook_deps.map { |arr| arr.join('->') }.join("\n")}\n}" +end + +def output + to_s +end diff --git a/lib/bookworm/reports/DynamicRecipeInclusion.rb b/lib/bookworm/reports/DynamicRecipeInclusion.rb new file mode 100644 index 0000000..9895453 --- /dev/null +++ b/lib/bookworm/reports/DynamicRecipeInclusion.rb @@ -0,0 +1,29 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Determines all recipes using dynamic recipe inclusion (ie not string literals)' +needs_rules ['IncludeRecipeDynamic'] + +def to_a + buffer = [] + @kb.recipes.each do |recipe, metadata| + buffer << recipe if metadata['IncludeRecipeDynamic'] + end + buffer.sort! + buffer +end + +def output + to_a +end diff --git a/lib/bookworm/reports/LeafCookbooks.rb b/lib/bookworm/reports/LeafCookbooks.rb new file mode 100644 index 0000000..203a194 --- /dev/null +++ b/lib/bookworm/reports/LeafCookbooks.rb @@ -0,0 +1,30 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Determine all leaf cookbooks, where no other cookbook depends on it via metadata.rb' +needs_rules ['ExplicitMetadataDepends'] + +def to_a + buffer = Set.new + @kb.metadatarbs.each do |_, metadata| + metadata['ExplicitMetadataDepends'].each do |cb| + buffer << cb + end + end + (@kb.cookbooks.keys.to_set - buffer).sort.to_a +end + +def output + to_a +end diff --git a/lib/bookworm/reports/LibraryDefinedModulesAndClassConstants.rb b/lib/bookworm/reports/LibraryDefinedModulesAndClassConstants.rb new file mode 100644 index 0000000..0b18a8e --- /dev/null +++ b/lib/bookworm/reports/LibraryDefinedModulesAndClassConstants.rb @@ -0,0 +1,27 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Get modules and classes constants that are explicitly defined in library files' +needs_rules ['LibraryDefinedClassConstants', 'LibraryDefinedModuleConstants'] + +def output + buffer = '' + buffer << "file\tmodules\tconstants\n" + @kb.libraries.sort_by { |k, _| k }.each do |name, metadata| + # buffer << name + buffer << "#{name}:\t#{metadata['LibraryDefinedModuleConstants'].join(',')}" + + "\t#{metadata['LibraryDefinedClassConstants'].join(',')}\n" + end + buffer +end diff --git a/lib/bookworm/reports/MissingReferencedRecipes.rb b/lib/bookworm/reports/MissingReferencedRecipes.rb new file mode 100644 index 0000000..8c9c027 --- /dev/null +++ b/lib/bookworm/reports/MissingReferencedRecipes.rb @@ -0,0 +1,61 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Determines all recipes that are directly referenced in roles and recipes' + + ' but were not found by the crawler' +needs_rules ['RoleRunListRecipes', 'IncludeRecipeLiterals'] + +def to_h + known_recipes = Set.new @kb.recipes.keys + missing = { 'roles' => {}, 'recipes' => {} } + @kb.roles.each do |role, metadata| + metadata['RoleRunListRecipes'].each do |recipe| + unless known_recipes.include? recipe + missing['roles'][role] ||= [] + missing['roles'][role].append recipe + end + end + end + @kb.recipes.each do |kbrecipe, metadata| + metadata['IncludeRecipeLiterals'].each do |recipe| + unless known_recipes.include? recipe + missing['recipes'][kbrecipe] ||= [] + missing['recipes'][kbrecipe].append recipe + end + end + end + missing +end + +def to_plain + hsh = to_h + buffer = '' + if hsh['roles'].empty? + buffer << "No missing recipes coming from the roles files\n" + else + buffer << "Roles:\n" + hsh['roles'].each do |role, recipes| + buffer << "\t#{role}\t#{recipes.join(', ')}\n" + end + end + if hsh['recipes'].empty? + buffer << 'No missing recipes coming from the recipe files' + else + buffer << "Recipes:\n" + hsh['recipes'].each do |kbrecipe, recipes| + buffer << "\t#{kbrecipe}\t#{recipes.join(', ')}\n" + end + end + buffer +end diff --git a/lib/bookworm/reports/NoParsedRuby.rb b/lib/bookworm/reports/NoParsedRuby.rb new file mode 100644 index 0000000..2a5e01f --- /dev/null +++ b/lib/bookworm/reports/NoParsedRuby.rb @@ -0,0 +1,46 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Ruby files which are empty, comments-only, ' + + 'or unparseable by RuboCop' +needs_rules ['NoParsedRuby'] + +def to_h + no_parsed_ruby = {} + Bookworm::InferRules::NoParsedRuby.keys.each do |key| + plural = BOOKWORM_KEYS[key]['plural'] + no_parsed_ruby[plural] = [] + @kb.send(plural.to_sym).each do |k, metadata| + no_parsed_ruby[plural].append(k) if metadata['NoParsedRuby'] + end + end + no_parsed_ruby +end + +def to_plain + hsh = to_h + buffer = '' + Bookworm::InferRules::NoParsedRuby.keys.each do |key| + plural = BOOKWORM_KEYS[key]['plural'] + if hsh[plural].empty? + buffer << "No non-AST #{plural} files\n" + else + buffer << "#{plural.capitalize}:\n" + hsh[plural].sort.each do |keys| + buffer << "\t#{keys}\n" + end + end + end + buffer +end diff --git a/lib/bookworm/reports/NotReferencedRecipes.rb b/lib/bookworm/reports/NotReferencedRecipes.rb new file mode 100644 index 0000000..c552b2d --- /dev/null +++ b/lib/bookworm/reports/NotReferencedRecipes.rb @@ -0,0 +1,37 @@ +# Copyright (c) 2023-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Determines all recipes that are not directly referenced either in roles or recipes' +needs_rules ['RoleRunListRecipes', 'IncludeRecipeLiterals'] + +def to_a + referenced_recipes = Set.new + @kb.roles.each do |_, metadata| + metadata['RoleRunListRecipes'].each do |role| + referenced_recipes << role + end + end + @kb.recipes.each do |_, metadata| + metadata['IncludeRecipeLiterals'].each do |recipe| + referenced_recipes << recipe + end + end + all_recipes = Set.new(@kb.recipes.keys) + # Any recipes which weren't included via roles or include_recipe are not referenced + (all_recipes - referenced_recipes).sort.to_a +end + +def output + to_a +end diff --git a/lib/bookworm/reports/RecipesAssigningConstants.rb b/lib/bookworm/reports/RecipesAssigningConstants.rb new file mode 100644 index 0000000..9046eeb --- /dev/null +++ b/lib/bookworm/reports/RecipesAssigningConstants.rb @@ -0,0 +1,27 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Determine all recipes that are assigning a constant in the recipe file' +needs_rules ['RecipeConstantAssignments'] + +def output + buffer = '' + + @kb.recipes.each do |name, metadata| + unless metadata['RecipeConstantAssignments'].empty? + buffer << "#{name} #{metadata['RecipeConstantAssignments'].join(', ')}\n" + end + end + buffer +end diff --git a/lib/bookworm/reports/RoleRecipeEntrypoints.rb b/lib/bookworm/reports/RoleRecipeEntrypoints.rb new file mode 100644 index 0000000..d97f37e --- /dev/null +++ b/lib/bookworm/reports/RoleRecipeEntrypoints.rb @@ -0,0 +1,30 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Determines all recipes that are directly referenced by all roles' +needs_rules ['RoleRunListRecipes'] + +def to_a + buffer = Set.new + @kb.roles.each do |_, metadata| + metadata['RoleRunListRecipes'].each do |recipe| + buffer << recipe + end + end + buffer.sort.to_a +end + +def output + to_a +end diff --git a/lib/bookworm/reports/RoleReferencedRoles.rb b/lib/bookworm/reports/RoleReferencedRoles.rb new file mode 100644 index 0000000..2965fd9 --- /dev/null +++ b/lib/bookworm/reports/RoleReferencedRoles.rb @@ -0,0 +1,30 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Determine roles which are directly referenced by other roles' +needs_rules ['RoleExplicitRoles'] + +def to_a + buffer = Set.new + @kb.roles.each do |_, metadata| + metadata['RoleExplicitRoles'].each do |role| + buffer << role + end + end + buffer.sort.to_a +end + +def output + to_a +end diff --git a/lib/bookworm/rules/ExplicitMetadataDepends.rb b/lib/bookworm/rules/ExplicitMetadataDepends.rb new file mode 100644 index 0000000..e12a1c7 --- /dev/null +++ b/lib/bookworm/rules/ExplicitMetadataDepends.rb @@ -0,0 +1,22 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Extract depends usage from a cookbook\'s metadata.rb' +keys ['metadatarb'] + +def_node_search :explicit_depends, '`(send nil? :depends (str $_))' + +def to_a + explicit_depends(@metadata['ast']).to_a.uniq +end diff --git a/lib/bookworm/rules/IncludeRecipeDynamic.rb b/lib/bookworm/rules/IncludeRecipeDynamic.rb new file mode 100644 index 0000000..7286ca0 --- /dev/null +++ b/lib/bookworm/rules/IncludeRecipeDynamic.rb @@ -0,0 +1,24 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Extracts recipes that do not use include_recipe with a string literal' +keys ['recipe'] + +def_node_search :include_recipe_dynamic, '`(send nil? :include_recipe $_)' + +def output + include_recipe_dynamic(@metadata['ast']).any? do |x| + !(x.is_a?(RuboCop::AST::StrNode) && x.str_type?) + end +end diff --git a/lib/bookworm/rules/IncludeRecipeLiterals.rb b/lib/bookworm/rules/IncludeRecipeLiterals.rb new file mode 100644 index 0000000..c3e9f04 --- /dev/null +++ b/lib/bookworm/rules/IncludeRecipeLiterals.rb @@ -0,0 +1,37 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Extracts recipes that are used by include_recipe with string literals' +keys ['recipe'] + +def_node_search :include_recipe_string_literals, '`(send nil? :include_recipe (str $_))' + +def to_a + arr = [] + include_recipe_string_literals(@metadata['ast']).each do |x| + arr << x + end + return [] if arr.empty? + arr.map! do |x| + if x.start_with?('::') + "#{@metadata['cookbook']}#{x}" + elsif !x.include?('::') + "#{x}::default" + else + x + end + end + arr.uniq! + arr.sort! +end diff --git a/lib/bookworm/rules/LibraryDefinedClassConstants.rb b/lib/bookworm/rules/LibraryDefinedClassConstants.rb new file mode 100644 index 0000000..afef981 --- /dev/null +++ b/lib/bookworm/rules/LibraryDefinedClassConstants.rb @@ -0,0 +1,22 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Get all class constants - it does *not* qualify the namespace' +keys ['library'] + +def_node_search :defined_class_constants, '`(class (const nil? $_) ...)' + +def to_a + defined_class_constants(@metadata['ast']).to_a.uniq +end diff --git a/lib/bookworm/rules/LibraryDefinedModuleConstants.rb b/lib/bookworm/rules/LibraryDefinedModuleConstants.rb new file mode 100644 index 0000000..53d0a4c --- /dev/null +++ b/lib/bookworm/rules/LibraryDefinedModuleConstants.rb @@ -0,0 +1,22 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Get all module constants - it does *not* qualify the namespace' +keys ['library'] + +def_node_search :defined_module_constants, '`(module (const nil? $_) ...)' + +def to_a + defined_module_constants(@metadata['ast']).to_a.uniq +end diff --git a/lib/bookworm/rules/NoParsedRuby.rb b/lib/bookworm/rules/NoParsedRuby.rb new file mode 100644 index 0000000..ef3ffde --- /dev/null +++ b/lib/bookworm/rules/NoParsedRuby.rb @@ -0,0 +1,30 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Ruby files which are empty, comments-only, ' + + 'or unparseable by RuboCop' +keys %w{ + attribute + library + metadatarb + provider + recipe + resource + role +} + +# See note in crawler.rb about EMPTY_RUBOCOP_AST constant +def output + @metadata['ast'] == Bookworm::Crawler::EMPTY_RUBOCOP_AST +end diff --git a/lib/bookworm/rules/RecipeConstantAssignments.rb b/lib/bookworm/rules/RecipeConstantAssignments.rb new file mode 100644 index 0000000..1fcbdb8 --- /dev/null +++ b/lib/bookworm/rules/RecipeConstantAssignments.rb @@ -0,0 +1,22 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Extract constants that are defined in recipes' +keys ['recipe'] + +def_node_search :constants_assigned, '`(casgn nil? $_ ...)' + +def to_a + constants_assigned(@metadata['ast']).to_a.uniq.sort +end diff --git a/lib/bookworm/rules/RoleDescription.rb b/lib/bookworm/rules/RoleDescription.rb new file mode 100644 index 0000000..e2d8eeb --- /dev/null +++ b/lib/bookworm/rules/RoleDescription.rb @@ -0,0 +1,22 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Scrapes description from role file' +keys ['role'] + +def_node_matcher :role_description, '`(send nil? :description (str $_))' + +def output + role_description @metadata['ast'] +end diff --git a/lib/bookworm/rules/RoleExplicitRoles.rb b/lib/bookworm/rules/RoleExplicitRoles.rb new file mode 100644 index 0000000..1070be1 --- /dev/null +++ b/lib/bookworm/rules/RoleExplicitRoles.rb @@ -0,0 +1,29 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Scrapes explicit roles in a role files run list' +keys ['role'] + +def_node_matcher :role_run_list, '`(send nil? :run_list (str $_)*)' + +def output + arr = role_run_list @metadata['ast'] + roles = [] + arr&.each do |item| + roles << item if item.start_with? 'role[' + end + # TODO(dcrosby) better regex here + roles.map! { |x| x.gsub(/^role\[/, '') } + roles.map { |x| x.gsub(/\]$/, '') } +end diff --git a/lib/bookworm/rules/RoleName.rb b/lib/bookworm/rules/RoleName.rb new file mode 100644 index 0000000..cca69cf --- /dev/null +++ b/lib/bookworm/rules/RoleName.rb @@ -0,0 +1,22 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Scrapes name from role file' +keys ['role'] + +def_node_matcher :role_name, '`(send nil? :name (str $_))' + +def output + role_name @metadata['ast'] +end diff --git a/lib/bookworm/rules/RoleRunList.rb b/lib/bookworm/rules/RoleRunList.rb new file mode 100644 index 0000000..63a772c --- /dev/null +++ b/lib/bookworm/rules/RoleRunList.rb @@ -0,0 +1,22 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Scrapes run list from role file' +keys ['role'] + +def_node_matcher :role_run_list, '`(send nil? :run_list (str $_)*)' + +def output + role_run_list @metadata['ast'] +end diff --git a/lib/bookworm/rules/RoleRunListRecipes.rb b/lib/bookworm/rules/RoleRunListRecipes.rb new file mode 100644 index 0000000..4765d98 --- /dev/null +++ b/lib/bookworm/rules/RoleRunListRecipes.rb @@ -0,0 +1,29 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +description 'Scrapes run list recipes from role file' +keys ['role'] + +def_node_matcher :role_run_list, '`(send nil? :run_list (str $_)*)' + +def output + arr = role_run_list @metadata['ast'] + recipes = [] + arr&.each do |item| + recipes << item unless item.start_with? 'role[' + end + + recipes.map! { |x| x.start_with?('recipe[') ? x.match(/recipe\[(.*)\]/)[1] : x } + recipes.map { |x| x.include?('::') ? x : "#{x}::default" } +end diff --git a/spec/knowledge_base_spec.rb b/spec/knowledge_base_spec.rb new file mode 100644 index 0000000..e4773ae --- /dev/null +++ b/spec/knowledge_base_spec.rb @@ -0,0 +1,69 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative 'spec_helper' +require_relative '../lib/bookworm/keys' +require_relative '../lib/bookworm/knowledge_base' + +describe Bookworm::KnowledgeBase do + it 'holds all yer roles' do + kb = Bookworm::KnowledgeBase.new({ + 'role' => [['foo.rb', '(ast)']], + }) + expect(kb.roles).to eq({ 'foo' => { 'path' => 'foo.rb', 'ast'=> '(ast)' } }) + end + it 'holds all yer metadatarbs' do + kb = Bookworm::KnowledgeBase.new({ + 'metadatarb' => [['thing/metadata.rb', '(ast)']], + }) + expect(kb.cookbooks).to eq({ 'thing' => {} }) + expect(kb.metadatarbs).to eq({ 'thing::metadata.rb' => { + 'path' => 'thing/metadata.rb', + 'cookbook' => 'thing', + 'ast' => '(ast)', + } }) + end + it 'holds all yer recipes' do + kb = Bookworm::KnowledgeBase.new({ + 'recipe'=> [['thing/recipes/default.rb', '(ast)']], + }) + expect(kb.recipes).to eq({ 'thing::default' => { + 'path' => 'thing/recipes/default.rb', + 'cookbook' => 'thing', + 'ast' => '(ast)', + } }) + end + it 'holds all yer attributes' do + kb = Bookworm::KnowledgeBase.new( + { + 'attribute' => [['thing/attributes/default.rb', '(ast)']], + }, + ) + expect(kb.attributes).to eq({ 'thing::default' => { + 'path' => 'thing/attributes/default.rb', + 'cookbook' => 'thing', + 'ast' => '(ast)', + } }) + end + it 'holds all yer libraries' do + kb = Bookworm::KnowledgeBase.new( + { 'library' => [['thing/libraries/default.rb', '(ast)']] }, + ) + expect(kb.libraries).to eq({ 'thing::default' => { + 'path' => 'thing/libraries/default.rb', + 'cookbook' => 'thing', + 'ast' => '(ast)', + } }) + end +end diff --git a/spec/rules/ExplicitMetadataDepends_spec.rb b/spec/rules/ExplicitMetadataDepends_spec.rb new file mode 100644 index 0000000..a20c20e --- /dev/null +++ b/spec/rules/ExplicitMetadataDepends_spec.rb @@ -0,0 +1,30 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative './helper' + +describe Bookworm::InferRules::ExplicitMetadataDepends do + let(:ast) do + generate_ast(<<~RUBY) + name 'fake_cookbook' + version '0.0.1' + depends 'fake_cookbook1' + depends 'fake_cookbook2' + RUBY + end + it 'captures the metadata.rb dependencies' do + rule = described_class.new({ 'ast' => ast }) + expect(rule.to_a).to eq(['fake_cookbook1', 'fake_cookbook2']) + end +end diff --git a/spec/rules/IncludeRecipeDynamic_spec.rb b/spec/rules/IncludeRecipeDynamic_spec.rb new file mode 100644 index 0000000..68eb45d --- /dev/null +++ b/spec/rules/IncludeRecipeDynamic_spec.rb @@ -0,0 +1,53 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative './helper' + +describe Bookworm::InferRules::IncludeRecipeDynamic do + it 'returns false on no AST' do + ast = generate_ast(<<~RUBY) + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq(false) + end + it 'returns false where no include_recipe found' do + ast = generate_ast(<<~RUBY) + file 'just_a_plain_old_resource' + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq(false) + end + it 'returns false where no dynamic include_recipe found' do + ast = generate_ast(<<~RUBY) + include_recipe '::fake_recipe' + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq(false) + end + it 'returns true where include_recipe with variable found' do + ast = generate_ast(<<~RUBY) + var = '::fake_recipe' + include_recipe var + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq(true) + end + it 'returns true where include_recipe with interpolated string found' do + ast = generate_ast(<<~RUBY) + include_recipe "\#{cookbook_name}::fake_recipe" + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq(true) + end +end diff --git a/spec/rules/IncludeRecipeLiterals_spec.rb b/spec/rules/IncludeRecipeLiterals_spec.rb new file mode 100644 index 0000000..edf4a7c --- /dev/null +++ b/spec/rules/IncludeRecipeLiterals_spec.rb @@ -0,0 +1,59 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative './helper' + +describe Bookworm::InferRules::IncludeRecipeLiterals do + it 'returns empty array when no include_recipe' do + ast = generate_ast(<<~RUBY) + file 'just_a_plain_old_resource' + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq([]) + end + it 'returns qualified recipe name with :: prefix sugar' do + ast = generate_ast(<<~RUBY) + include_recipe '::fake_recipe' + RUBY + rule = described_class.new({ + 'cookbook' => 'fake_cookbook', + 'ast' => ast, + }) + expect(rule.output).to eq(['fake_cookbook::fake_recipe']) + end + it 'returns qualified recipe name with implied default recipe' do + ast = generate_ast(<<~RUBY) + include_recipe 'fake_cookbook' + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq(['fake_cookbook::default']) + end + it 'returns qualified recipe name with qualified recipe name' do + ast = generate_ast(<<~RUBY) + include_recipe 'fake_cookbook::fake_recipe' + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq(['fake_cookbook::fake_recipe']) + end + it 'handles multiple include_recipe calls' do + ast = generate_ast(<<~RUBY) + file 'stub' # <- some code to test AST recursion + include_recipe 'fake_cookbook::foo' + include_recipe 'fake_cookbook::bar' + RUBY + rule = described_class.new({ 'ast' => ast }) + # Note - output is sorted + expect(rule.output).to eq(['fake_cookbook::bar', 'fake_cookbook::foo']) + end +end diff --git a/spec/rules/LibraryDefinedClassConstants_spec.rb b/spec/rules/LibraryDefinedClassConstants_spec.rb new file mode 100644 index 0000000..ba356ab --- /dev/null +++ b/spec/rules/LibraryDefinedClassConstants_spec.rb @@ -0,0 +1,50 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative './helper' + +describe Bookworm::InferRules::LibraryDefinedClassConstants do + it 'returns empty array when no class defined' do + ast = generate_ast(<<~RUBY) + true + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq([]) + end + it 'captures the top-level class name' do + ast = generate_ast(<<~RUBY) + class Foo ; end + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq([:Foo]) + end + it 'captures class name inside module' do + ast = generate_ast(<<~RUBY) + module Bar + class Foo ; end + end + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq([:Foo]) + end + it 'captures nested class names' do + ast = generate_ast(<<~RUBY) + class Bar + class Foo ; end + end + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq([:Bar, :Foo]) + end +end diff --git a/spec/rules/LibraryDefinedModuleConstants_spec.rb b/spec/rules/LibraryDefinedModuleConstants_spec.rb new file mode 100644 index 0000000..87575d7 --- /dev/null +++ b/spec/rules/LibraryDefinedModuleConstants_spec.rb @@ -0,0 +1,50 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative './helper' + +describe Bookworm::InferRules::LibraryDefinedModuleConstants do + it 'returns empty array when no module defined' do + ast = generate_ast(<<~RUBY) + true + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq([]) + end + it 'captures the top-level module name' do + ast = generate_ast(<<~RUBY) + module Foo ; end + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq([:Foo]) + end + it 'captures module name inside module' do + ast = generate_ast(<<~RUBY) + module Bar + class Foo ; end + end + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq([:Bar]) + end + it 'captures nested module names' do + ast = generate_ast(<<~RUBY) + module Bar + module Foo ; end + end + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq([:Bar, :Foo]) + end +end diff --git a/spec/rules/NoParsedRuby_spec.rb b/spec/rules/NoParsedRuby_spec.rb new file mode 100644 index 0000000..e53ff2d --- /dev/null +++ b/spec/rules/NoParsedRuby_spec.rb @@ -0,0 +1,36 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative './helper' + +describe Bookworm::InferRules::NoParsedRuby do + let(:no_ast) do + generate_ast(<<~RUBY) + # some silly comment + RUBY + end + let(:has_ast) do + generate_ast(<<~RUBY) + name "fake_role" + RUBY + end + it 'returns true if does not have ast' do + rule = described_class.new({ 'ast' => no_ast }) + expect(rule.output).to eq(true) + end + it 'returns false if has ast' do + rule = described_class.new({ 'ast' => has_ast }) + expect(rule.output).to eq(false) + end +end diff --git a/spec/rules/RecipeConstantAssignments_spec.rb b/spec/rules/RecipeConstantAssignments_spec.rb new file mode 100644 index 0000000..4ca6405 --- /dev/null +++ b/spec/rules/RecipeConstantAssignments_spec.rb @@ -0,0 +1,41 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative './helper' + +describe Bookworm::InferRules::RecipeConstantAssignments do + it 'does not capture normal variables' do + ast= generate_ast(<<~RUBY) + foo = "bar" + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq([]) + end + it 'captures a single constant assignment' do + ast = generate_ast(<<~RUBY) + Foo = "bar" + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq([:Foo]) + end + it 'captures a multiple constant assignments' do + ast = generate_ast(<<~RUBY) + FooB = "bar" + FooA = "bar" + FooC = "bar" + RUBY + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq([:FooA, :FooB, :FooC]) + end +end diff --git a/spec/rules/RoleDescription_spec.rb b/spec/rules/RoleDescription_spec.rb new file mode 100644 index 0000000..2fc28b8 --- /dev/null +++ b/spec/rules/RoleDescription_spec.rb @@ -0,0 +1,27 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative './helper' + +describe Bookworm::InferRules::RoleDescription do + let(:ast) do + generate_ast(<<~RUBY) + description "This is a fake role" + RUBY + end + it 'captures the role description' do + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq('This is a fake role') + end +end diff --git a/spec/rules/RoleExplicitRoles_spec.rb b/spec/rules/RoleExplicitRoles_spec.rb new file mode 100644 index 0000000..81a20f4 --- /dev/null +++ b/spec/rules/RoleExplicitRoles_spec.rb @@ -0,0 +1,33 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative './helper' + +describe Bookworm::InferRules::RoleExplicitRoles do + let(:ast) do + generate_ast(<<~RUBY) + name "fake role" + description "This is a fake role" + + run_list( + 'role[foo]', + 'recipe[bar]', + ) + RUBY + end + it 'captures the roles from the run list' do + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq(['foo']) + end +end diff --git a/spec/rules/RoleName_spec.rb b/spec/rules/RoleName_spec.rb new file mode 100644 index 0000000..afa1077 --- /dev/null +++ b/spec/rules/RoleName_spec.rb @@ -0,0 +1,27 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative './helper' + +describe Bookworm::InferRules::RoleName do + let(:ast) do + generate_ast(<<~RUBY) + name "fake_role" + RUBY + end + it 'captures the role name' do + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq('fake_role') + end +end diff --git a/spec/rules/RoleRunListRecipes_spec.rb b/spec/rules/RoleRunListRecipes_spec.rb new file mode 100644 index 0000000..e8dde27 --- /dev/null +++ b/spec/rules/RoleRunListRecipes_spec.rb @@ -0,0 +1,34 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative 'helper' + +describe Bookworm::InferRules::RoleRunListRecipes do + let(:ast) do + generate_ast(<<~RUBY) + name "fake role" + description "This is a fake role" + + run_list( + 'role[foo]', + 'recipe[bar]', + 'recipe[baz::packages]', + ) + RUBY + end + it 'captures the recipe names' do + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq(['bar::default', 'baz::packages']) + end +end diff --git a/spec/rules/RoleRunList_spec.rb b/spec/rules/RoleRunList_spec.rb new file mode 100644 index 0000000..ce2a94f --- /dev/null +++ b/spec/rules/RoleRunList_spec.rb @@ -0,0 +1,33 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require_relative './helper' + +describe Bookworm::InferRules::RoleRunList do + let(:ast) do + generate_ast(<<~RUBY) + name "fake role" + description "This is a fake role" + + run_list( + 'role[foo]', + 'recipe[bar]', + ) + RUBY + end + it 'captures the run list from a role' do + rule = described_class.new({ 'ast' => ast }) + expect(rule.output).to eq(['role[foo]', 'recipe[bar]']) + end +end diff --git a/spec/rules/helper.rb b/spec/rules/helper.rb new file mode 100644 index 0000000..d292de2 --- /dev/null +++ b/spec/rules/helper.rb @@ -0,0 +1,32 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require 'pathname' +require_relative '../../lib/bookworm/crawler' +require_relative '../../lib/bookworm/exceptions' +require_relative '../../lib/bookworm/infer_engine' + +# Load all rule files +files = Dir.glob("#{__dir__}/../../lib/bookworm/rules/*.rb") +files.each do |f| + name = Pathname(f).basename.to_s.gsub('.rb', '') + ::Bookworm.load_rule_class name, :dir => "#{__dir__}/../../lib/bookworm/rules" +end + +require_relative '../spec_helper' + +def generate_ast(str) + ::RuboCop::ProcessedSource.new(str, RUBY_VERSION.to_f)&.ast || + ::Bookworm::Crawler::EMPTY_RUBOCOP_AST +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..3719c64 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,23 @@ +# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require 'rspec' +require 'rspec/support' +require 'rspec/support/differ' +RSpec.configure do |config| + # Basic configuration + config.run_all_when_everything_filtered = true + config.filter_run(:focus) + config.order = :random +end