module Sequel::Plugins::UnusedAssociations

  1. lib/sequel/plugins/unused_associations.rb

The unused_associations plugin detects which model associations are not used and can be removed, and which model association methods are not used and can skip being defined. The advantage of removing unused associations and unused association methods is decreased memory usage, since each method defined takes memory and adds more work for the garbage collector.

In order to detect which associations are used, this relies on the method coverage support added in Ruby 2.5. To allow flexibility to override association methods, the association methods that Sequel defines are defined in a module included in the class instead of directly in the class. Unfortunately, that makes it difficult to directly use the coverage data to find unused associations. The advantage of this plugin is that it is able to figure out from the coverage information whether the association methods Sequel defines are actually used.

Basic Usage

The expected usage of the unused_associations plugin is to load it into the base class for models in your application, which will often be Sequel::Model:

Sequel::Model.plugin :unused_associations

Then you run your test suite with method coverage enabled, passing the coverage result to update_associations_coverage. update_associations_coverage returns a data structure containing method coverage information for all subclasses of the base class. You can pass the coverage information to update_unused_associations_data, which will return a data structure with information on unused associations.

require 'coverage'
Coverage.start(methods: true)
# load sequel after starting coverage, then run your tests
cov_data = Sequel::Model.update_associations_coverage
unused_associations_data = Sequel::Model.update_unused_associations_data(coverage_data: cov_data)

You can take that unused association data and pass it to the unused_associations method to get a array of information on associations which have not been used. Each entry in the array will contain a class name and association name for each unused association, both as a string:

Sequel::Model.unused_associations(unused_associations_data: unused_associations_data)
# => [["Class1", "assoc1"], ...]

You can use the output of the unused_associations method to determine which associations are not used at all in your application, and can be eliminiated.

You can also take that unused association data and pass it to the unused_association_options method, which will return an array of information on associations which are used, but have related methods defined that are not used. The first two entries in each array are the class name and association name as a string, and the third entry is a hash of association options:

Sequel::Model.unused_association_options(unused_associations_data: unused_associations_data)
# => [["Class2", "assoc2", {:read_only=>true}], ...]

You can use the output of the unused_association_options to find out which association options can be provided when defining the association so that the association method will not define methods that are not used.

Combining Coverage Results

It is common to want to combine results from multiple separate coverage runs. For example, if you have multiple test suites for your application, one for model or unit tests and one for web or integration tests, you would want to combine the coverage information from all test suites before determining that the associations are not used.

The unused_associations plugin supports combining multiple coverage results using the :coverage_file plugin option:

Sequel::Model.plugin :unused_associations,
  coverage_file: 'unused_associations_coverage.json'

With the coverage file option, update_associations_coverage will look in the given file for existing coverage information, if it exists. If the file exists, the data from it will be merged with the coverage result passed to the method. Before returning, the coverage file will be updated with the merged result. When using the :coverage_file plugin option, you can each of your test suites update the coverage information:

require 'coverage'
Coverage.start(methods: true)
# run this test suite
Sequel::Model.update_associations_coverage

After all test suites have been run, you can run update_unused_associations_data, without an argument:

unused_associations_data = Sequel::Model.update_unused_associations_data

With no argument, update_unused_associations_data will get the coverage data from the coverage file, and then use that to prepare the information. You can then use the returned value the same as before to get the data on unused associations. To prevent stale coverage information, calling update_unused_associations_data when using the :coverage_file plugin option will remove the coverage file by default (you can use the :keep_coverage option to prevent the deletion of the coverage file).

Automatic Usage of Unused Association Data

Since it can be a pain to manually update all of your code to remove unused assocations or add options to prevent the definition of unused associations, the unused_associations plugin comes with support to take previously saved unused association data, and use it to not create unused associations, and to automatically use the appropriate options so that unused association methods are not created.

To use this option, you first need to save the unused association data previously prepared. You can do this by passing an :file option when loading the plugin.

Sequel::Model.plugin :unused_associations,
  file: 'unused_associations.json'

With the :file option provided, you no longer need to use the return value of update_unused_associations_data, as the file will be updated with the information:

Sequel::Model.update_unused_associations_data(coverage_data: cov_data)

Then, to use the saved unused associations data, add the :modify_associations plugin option:

Sequel::Model.plugin :unused_associations,
  file: 'unused_associations.json',
  modify_associations: true

With the :modify_associations used, and the unused association data file is available, when subclasses attempt to create an unused association, the attempt will be ignored. If the subclasses attempt to create an association where not all association methods are used, the plugin will automatically set the appropriate options so that the unused association methods are not defined.

When you are testing which associations are used, make sure not to set the :modify_associations plugin option, or make sure that the unused associations data file does not exist.

Automatic Usage with Combined Coverage Results

If you have multiple test suites and want to automatically use the unused association data, you should provide both :file and :coverage_file options when loading the plugin:

Sequel::Model.plugin :unused_associations,
  file: 'unused_associations.json',
  coverage_file: 'unused_associations_coverage.json'

Then each test suite just needs to run update_associations_coverage to update the coverage information:

Sequel::Model.update_associations_coverage

After all test suites have been run, you can run update_unused_associations_data to update the unused association data file (and remove the coverage file):

Sequel::Model.update_unused_associations_data

Then you can add the :modify_associations plugin option to automatically use the unused association data.

Caveats

Since this plugin is based on coverage information, if you do not have tests that cover all usage of associations in your application, you can end up with coverage that shows the association is not used, when it is used in code that is not covered. The output of plugin can still be useful in such cases, as long as you are manually checking it. However, you should avoid using the :modify_associations unless you have confidence that your tests cover all usage of associations in your application. You can specify the :is_used association option for any association that you know is used. If an association uses the :is_used association option, this plugin will not modify it if the :modify_associations option is used.

This plugin does not handle anonymous classes. Any unused associations defined in anonymous classes will not be reported by this plugin.

This plugin only considers the public instance methods the association defines, and direct access to the related association reflection via Sequel::Model.association_reflection to determine if the association was used. If the association metadata was accessed another way, it’s possible this plugin will show the association as unused.

As this relies on the method coverage added in Ruby 2.5, it does not work on older versions of Ruby. It also does not work on JRuby, as JRuby does not implement method coverage.

Methods

Public Class

  1. apply
  2. configure

Public Class methods

apply(mod, opts=OPTS)

Load the subclasses plugin, as the unused associations plugin is designed to handle all subclasses of the class it is loaded into.

[show source]
    # File lib/sequel/plugins/unused_associations.rb
230 def self.apply(mod, opts=OPTS)
231   mod.plugin :subclasses
232 end
configure(mod, opts=OPTS)

Plugin options:

:coverage_file

The file to store the coverage information, when combining coverage information from multiple test suites.

:file

The file to store and/or load the unused associations data.

:modify_associations

Whether to use the unused associations data to skip defining associations or association methods.

:unused_associations_data

The unused associations data to use if the :modify_associations is used (by default, the :modify_associations option will use the data from the file specified by the :file option). This is same data returned by the update_unused_associations_data method.

[show source]
    # File lib/sequel/plugins/unused_associations.rb
248 def self.configure(mod, opts=OPTS)
249   mod.instance_exec do
250     @unused_associations_coverage_file = opts[:coverage_file]
251     @unused_associations_file = opts[:file]
252     @unused_associations_data = if opts[:modify_associations]
253       if opts[:unused_associations_data]
254         opts[:unused_associations_data]
255       elsif File.file?(opts[:file])
256         Sequel.parse_json(File.binread(opts[:file]))
257       end
258     end
259   end
260 end