Project Index

The project index feature was introduced in RuboCop 1.87.
This feature is experimental and should not be considered stable. Changes to its behavior or interface may occur.

RuboCop can optionally use Rubydex to build a project-wide index of declarations and references. When enabled, cops that opt in can consult the index to detect issues that span multiple files.

This integration is opt-in and experimental. The default behavior of RuboCop is unchanged.

Enabling

  1. Add rubydex to your Gemfile and bundle install:

    gem 'rubydex', require: false
  2. Set the flag in your .rubocop.yml:

    AllCops:
      UseProjectIndex: true

If UseProjectIndex is true but the rubydex gem is not installed, or the running Ruby is older than the version rubydex supports, RuboCop prints a warning and falls back to its standard file-local behavior.

The integration requires Ruby 3.2 or later. On Ruby 3.1 and older, AllCops/UseProjectIndex has no effect even if set to true.

Indexing gem sources

By default the index covers only the project’s own files, so ancestry chains and members that live in gems are unresolvable, and index-aware cops fall back to their conservative behavior whenever one is involved (e.g. a class inheriting from a framework base class). Setting AllCops/ProjectIndexIncludesGems: true additionally indexes the sources of every gem in the project’s bundle:

AllCops:
  UseProjectIndex: true
  ProjectIndexIncludesGems: true

This makes ancestry-based reasoning conclusive for most real-world classes (on RuboCop’s own repository it raises the share of classes with a fully resolvable ancestry from about 20% to about 97%) at the cost of extra memory proportional to the bundle’s size and a slightly longer index build. The option requires RuboCop to run inside Bundler; outside a bundle it silently degrades to project-only indexing.

What it enables

Lint/ConstantReassignment reports reassignments whose previous definition lives in another file. For example:

# a.rb
CROSS_FILE_CONST = :first

# b.rb
CROSS_FILE_CONST = :second

With UseProjectIndex: true, RuboCop reports a Lint/ConstantReassignment offense referencing the other file.

Lint/DuplicateMethods reports methods whose duplicate definition lives in another file. For example:

# a.rb
class Foo
  def bar; end
end

# b.rb
class Foo
  def bar; end
end

With UseProjectIndex: true, RuboCop reports a Lint/DuplicateMethods offense in each file, referencing the definition in the other one. Redefining a method from another file on purpose (e.g. a monkey patch) can be signaled with the self-alias trick (alias bar bar right before the redefinition), which also suppresses Ruby’s method redefinition warning.

Lint/DeprecatedReference (pending) is entirely powered by the index: it reports calls to methods and references to constants documented with a YARD @deprecated tag anywhere in the project.

Lint/UnusedPrivateMethod (disabled by default) uses the index for project-wide dead-code detection: it reports private instance methods whose names are never referenced anywhere in the indexed project. Since symbol-based references from other files (e.g. Rails callbacks declared in a concern) cannot be detected, it is best suited for occasional dead-code sweeps.

Style/MissingRespondToMissing accepts a respond_to_missing? defined in another definition of the same class or module (e.g. a reopening in another file).

Lint/InheritException also reports classes that inherit from Exception indirectly, through a parent class defined elsewhere in the project.

Style/StaticClass does not report classes that are subclassed anywhere in the project.

Naming/PredicatePrefix and Naming/AccessorMethodName do not suggest renaming methods that override a method defined by an ancestor elsewhere in the project.

Style/ClassAndModuleChildren uses the index to make its (unsafe) autocorrection more reliable: when nesting a compact definition (class Foo::Bar), the namespace wrapper’s keyword (class or module) is resolved from the actual definition of Foo instead of guessed, and compacting a nested definition is skipped when the outer namespace is not defined elsewhere (the compact form would raise NameError at load time).

Lint/MissingSuper skips the constructor offense when the class' entire ancestry is resolvable in the index and no ancestor defines initialize - in that case super would only reach the no-op Object#initialize. This removes the need to list project-local abstract base classes in AllowedParentClasses.

Style/RedundantConstantBase also reports a leading :: inside a namespace when the constant provably resolves identically without it.

Lint/ConstantResolution reports only genuinely ambiguous constants - those that resolve to a different declaration through the surrounding nesting than they would fully qualified - which makes the cop practical to enable without Only/Ignore lists.

Style/Documentation accepts a reopened class or module when any of its other definition sites carries a documentation comment, instead of requiring a comment at every reopening.

Lint/NameTypo (pending) is entirely powered by the index: it reports qualified constant references and constant-receiver method calls that do not resolve anywhere in the project when a similarly named alternative exists, and suggests it. CheckConstants and CheckMethods toggle the two checks independently.

Lint/ArgumentMismatch (pending) reports calls on a constant receiver (Foo.bar) that pass the wrong number of positional arguments to a singleton method defined in the project, when the receiver and its whole ancestry are resolved in the index.

Lint/SuperArgumentMismatch (pending) reports super calls with explicit arguments that pass the wrong number of positional arguments to the overridden implementation, when the enclosing class and its whole ancestry are resolved in the index.

Index-aware cops automatically pick up the index whenever it is built; no per-cop opt-in is required.

Notes

  • rubydex requires Ruby 3.2 or newer and ships native (Rust) extensions.

  • Parallel inspection is currently disabled on Windows when UseProjectIndex is on; use serial inspection (omit --parallel).

  • The index always covers the whole project (rooted at the directory containing Gemfile or gems.rb, falling back to the current directory), regardless of which files a particular run inspects. Inspecting a single file therefore reports the same cross-file offenses as a full run.

  • The index is rebuilt once per rubocop invocation; no on-disk index is shared between runs. Indexing is fast (roughly 100ms for a couple thousand files), but very large monorepos will notice the per-run cost on single-file runs.

  • The language server is the exception: it builds the index once per session and reuses it across requests, rebuilding only when a file is saved or a watched file changes. Editing a buffer therefore does not pay the indexing cost on every keystroke.