README.md in tapioca-0.11.8 vs README.md in tapioca-0.11.9

- old
+ new

@@ -45,24 +45,23 @@ * [Using a .netrc file](#using-a-netrc-file) * [Changing the typed strictness of annotations files](#changing-the-typed-strictness-of-annotations-files) * [Generating RBI files for Rails and other DSLs](#generating-rbi-files-for-rails-and-other-dsls) * [Keeping RBI files for DSLs up-to-date](#keeping-rbi-files-for-dsls-up-to-date) * [Writing custom DSL compilers](#writing-custom-dsl-compilers) + * [Writing custom DSL extensions](#writing-custom-dsl-extensions) * [RBI files for missing constants and methods](#rbi-files-for-missing-constants-and-methods) - * [Generating the RBI file for missing constants](#generating-the-rbi-file-for-missing-constants) - * [Manually writing RBI definitions (shims)](#manually-writing-rbi-definitions-shims) * [Configuration](#configuration) * [Contributing](#contributing) * [License](#license) <!-- END_TOC --> ## Installation Add this line to your application's `Gemfile`: ```rb -group :development do +group :development, :test do gem 'tapioca', require: false end ``` Run `bundle install` and make sure Tapioca is properly installed: @@ -70,20 +69,20 @@ <!-- START_HELP --> ```shell $ tapioca help Commands: - tapioca --version, -v # show version + tapioca --version, -v # Show version tapioca annotations # Pull gem RBI annotations from remote sources - tapioca check-shims # check duplicated definitions in shim RBIs - tapioca configure # initialize folder structure and type checking configuration - tapioca dsl [constant...] # generate RBIs for dynamic methods - tapioca gem [gem...] # generate RBIs from gems + tapioca check-shims # Check duplicated definitions in shim RBIs + tapioca configure # Initialize folder structure and type checking configuration + tapioca dsl [constant...] # Generate RBIs for dynamic methods + tapioca gem [gem...] # Generate RBIs from gems tapioca help [COMMAND] # Describe available commands or one specific command - tapioca init # get project ready for type checking - tapioca require # generate the list of files to be required by tapioca - tapioca todo # generate the list of unresolved constants + tapioca init # Get project ready for type checking + tapioca require # Generate the list of files to be required by tapioca + tapioca todo # Generate the list of unresolved constants Options: -c, [--config=<config file path>] # Path to the Tapioca configuration file # Default: sorbet/tapioca/config.yml -V, [--verbose], [--no-verbose] # Verbose output for debugging purposes @@ -119,11 +118,11 @@ Options: -c, [--config=<config file path>] # Path to the Tapioca configuration file # Default: sorbet/tapioca/config.yml -V, [--verbose], [--no-verbose] # Verbose output for debugging purposes -get project ready for type checking +Get project ready for type checking ``` <!-- END_HELP_COMMAND_INIT --> ## Usage @@ -170,10 +169,11 @@ [--all], [--no-all] # Regenerate RBI files for all gems --pre, -b, [--prerequire=file] # A file to be required before Bundler.require is called --post, -a, [--postrequire=file] # A file to be required after Bundler.require is called # Default: sorbet/tapioca/require.rb -x, [--exclude=gem [gem ...]] # Exclude the given gem(s) from RBI generation + [--include-dependencies], [--no-include-dependencies] # Generate RBI files for dependencies of the given gem(s) --typed, -t, [--typed-overrides=gem:level [gem:level ...]] # Override for typed sigils for generated gem RBIs # Default: {"activesupport"=>"false"} [--verify], [--no-verify] # Verify RBIs are up-to-date [--doc], [--no-doc] # Include YARD documentation from sources when generating RBIs. Warning: this might be slow # Default: true @@ -194,15 +194,15 @@ # Default: true -c, [--config=<config file path>] # Path to the Tapioca configuration file # Default: sorbet/tapioca/config.yml -V, [--verbose], [--no-verbose] # Verbose output for debugging purposes -generate RBIs from gems +Generate RBIs from gems ``` <!-- END_HELP_COMMAND_GEM --> -By default, running `tapioca gem` will only generate the RBI files for gems that have been added to or removed from the project's `Gemfile` this means that Tapioca will not regenerate the RBI files for untouched gems. However, when changing Tapioca configuration or bumping its version, it may be useful to force the regeneration of the RBI files previously generated. This can be done with the `--all` option: +By default, running `tapioca gem` will only generate the RBI files for gems that have been added to or removed from the project's `Gemfile` this means that Tapioca will not regenerate the RBI files for untouched gems. If you want to force the regeneration you can supply gem names to the `tapioca gem` command. When supplying gem names if you want to generate RBI files for their dependencies as well, you can use the `--include-dependencies` option. When changing Tapioca configuration or bumping its version, it may be useful to force the regeneration of all the RBI files previously generated. This can be done with the `--all` option: ```shell bin/tapioca gems --all ``` @@ -481,11 +481,11 @@ # Default: true -c, [--config=<config file path>] # Path to the Tapioca configuration file # Default: sorbet/tapioca/config.yml -V, [--verbose], [--no-verbose] # Verbose output for debugging purposes -generate RBIs for dynamic methods +Generate RBIs for dynamic methods ``` <!-- END_HELP_COMMAND_DSL --> #### Keeping RBI files for DSLs up-to-date @@ -665,62 +665,126 @@ No errors! Great job. ``` For more concrete and advanced examples, take a look at [Tapioca's default DSL compilers](https://github.com/Shopify/tapioca/tree/main/lib/tapioca/dsl/compilers). -### RBI files for missing constants and methods +#### Writing custom DSL extensions -Even after generating the RBIs, it is possible that some constants or methods are still undefined for Sorbet. +When writing custom DSL compilers, it is sometimes necessary to rely on an extension, i.e. a bit of code that is being loaded before the application in order to override some behavior. This is typically useful when a DSL's implementation does not store enough information for the compiler to properly define signatures. -This might be for multiple reasons, with the most frequents ones being: +Let's reuse the previous `Encryptable` module as an example, but this time let's imagine that the implementation of `attr_encrypted` does not store attribute names: -* The constant or method comes from a part of the gem that Tapioca cannot load (optional dependency, wrong architecture, etc.) -* The constant or method comes from a DSL or meta-programming that Tapioca doesn't support yet -* The constant or method only exists when a specific code path is executed -The best way to deal with such occurrences is to manually create RBI files (shims) for them so you can also add types but depending on the amount of meta-programming used in your project this can mean an overwhelming amount of manual work. +```rb +module Encryptable + def self.included(base) + base.extend(ClassMethods) + end -#### Generating the RBI file for missing constants + module ClassMethods + def attr_encrypted(attr_name) + attr_accessor(attr_name) -To get you started quickly, Tapioca can create a RBI file containing a stub of all the missing constants so you can typecheck your project without missing constants and shim them later as you need them. + encrypted_attr_name = :"#{attr_name}_encrypted" -To generate the RBI file for the missing constants used in your application run the following command: + define_method(encrypted_attr_name) do + value = send(attr_name) + encrypt(value) + end -```shell -$ bin/tapioca todo + define_method("#{encrypted_attr_name}=") do |value| + send("#{attr_name}=", decrypt(value)) + end + end + end -Compiling sorbet/rbi/todo.rbi, this may take a few seconds... Done -All unresolved constants have been written to sorbet/rbi/todo.rbi. -Please review changes and commit them. + private + + def encrypt(value) + value.unpack("H*").first + end + + def decrypt(value) + [value].pack("H*") + end +end ``` -This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved constants as empty modules. Since the constants are "missing", Tapioca does not know if they should be marked as modules or classes and will use modules as a safer default. This file should be reviewed, corrected, if necessary, and then committed in your repository. +Without the `attribute_names` array, the compiler has no way of knowing which methods were defined by the `attr_encrypted` DSL. This can be solved by defining an extension that will override the behavior of `attr_encrypted`: -<!-- START_HELP_COMMAND_TODO --> -```shell -$ tapioca help todo +```rb +require "encryptable" -Usage: - tapioca todo +module Tapioca + module Extensions + module Encryptable + attr_reader :__tapioca_encrypted_attributes -Options: - [--todo-file=TODO_FILE] # Path to the generated todo RBI file - # Default: sorbet/rbi/todo.rbi - [--file-header], [--no-file-header] # Add a "This file is generated" header on top of each generated RBI file - # Default: true - -c, [--config=<config file path>] # Path to the Tapioca configuration file - # Default: sorbet/tapioca/config.yml - -V, [--verbose], [--no-verbose] # Verbose output for debugging purposes + def attr_encrypted(attr_name) + @__tapioca_encrypted_attributes ||= [] + @__tapioca_encrypted_attributes << attr_name.to_s -generate the list of unresolved constants + super + end + + ::Encryptable::ClassMethods.prepend(self) + end + end +end ``` -<!-- END_HELP_COMMAND_TODO --> -#### Manually writing RBI definitions (shims) +The compiler can now use the `__tapioca_encrypted_attributes` array managed by the extension: -A _shim_ is a hand-crafted RBI file that tells Sorbet about constants, ancestors, methods, etc. that it can't understand statically and aren't already generated by Tapioca. +```rb +module Tapioca + module Compilers + class Encryptable < Tapioca::Dsl::Compiler + extend T::Sig + ConstantType = type_member {{ fixed: T.class_of(Encryptable) }} + + sig { override.returns(T::Enumerable[Module]) } + def self.gather_constants + # Collect all the classes that include Encryptable + all_classes.select { |c| c < ::Encryptable } + end + + sig { override.void } + def decorate + # Create a RBI definition for each class that includes Encryptable + root.create_path(constant) do |klass| + # For each encrypted attribute we find in the class + constant.__tapioca_encrypted_attributes.each do |attr_name| + # Create the RBI definitions for all the missing methods + klass.create_method(attr_name, return_type: "String") + klass.create_method("#{attr_name}=", parameters: [ create_param("value", type: "String") ], return_type: "void") + klass.create_method("#{attr_name}_encrypted", return_type: "String") + klass.create_method("#{attr_name}_encrypted=", parameters: [ create_param("value", type: "String") ], return_type: "void") + end + end + end + end + end +end +``` + +In order for DSL extensions to be discovered by Tapioca, they either needs to be placed inside the `sorbet/tapioca/extensions` directory of your application or be inside a `tapioca/dsl/extensions` folder on the load path. + +For more concrete and advanced examples, take a look at [Tapioca's default DSL extensions](https://github.com/Shopify/tapioca/tree/main/lib/tapioca/dsl/extensions). + +### RBI files for missing constants and methods + +Even after generating the RBIs, it is possible that some constants or methods are still undefined for Sorbet. + +This might be for multiple reasons, with the most frequents ones being: + +* The constant or method comes from a part of the gem that Tapioca cannot load (optional dependency, wrong architecture, etc.) +* The constant or method comes from a DSL or meta-programming that Tapioca doesn't support yet +* The constant or method only exists when a specific code path is executed + +The best way to deal with such occurrences is _shims_. A shim is a hand-crafted RBI file that tells Sorbet about constants, ancestors, methods, etc. that it can't understand statically and aren't already generated by Tapioca. + These shims are usually placed in the `sorbet/rbi/shims` directory. From there, conventionally, you should follow the directory structure of the project to the file you'd like to shim. For example, say you had a `person.rb` file found at `app/models/person.rb`. If you were to add a shim for it, you'd want to create your RBI file at `sorbet/rbi/shims/app/models/person.rbi`. A shim might be as simple as the class definition with an empty method body as below: ```ruby @@ -780,14 +844,16 @@ -w, [--workers=N] # Number of parallel workers (default: auto) -c, [--config=<config file path>] # Path to the Tapioca configuration file # Default: sorbet/tapioca/config.yml -V, [--verbose], [--no-verbose] # Verbose output for debugging purposes -check duplicated definitions in shim RBIs +Check duplicated definitions in shim RBIs ``` <!-- END_HELP_COMMAND_CHECK_SHIMS --> +Depending on the amount of meta-programming used in your project this can mean an overwhelming amount of manual work. In this case, you should consider [writting a custom DSL compiler](#writing-custom-dsl-compilers). + ### Configuration Tapioca supports loading command defaults from a configuration file. The default configuration file location is `sorbet/tapioca/config.yml` but this default can be changed using the `--config` flag and supplying an alternative configuration file path. Tapioca's configuration file must be a well-formed YAML file with top-level keys for the various Tapioca commands. Keys under each such top-level command should be the underscore version of a long option name for that command and the value for that key should be the value of the option. @@ -837,9 +903,10 @@ file_header: true all: false prerequire: '' postrequire: sorbet/tapioca/require.rb exclude: [] + include_dependencies: false typed_overrides: activesupport: 'false' verify: false doc: true loc: true