Type Checking
NOTE: Type checking is a work in progress. Users are encouraged to suggest improvements and report bugs in the GitHub repo.
Solargraph provides a type checker that can idenfity problems like invalid tags, unrecognized methods, and mismatched types.
The type checker performs its diagnosis through a combination of YARD tag analysis and type inference. YARD tags help Solargraph determine the developer’s intention. Type inference helps determine what the code actually does. Varying degrees of type safety can be enforced, depending on the check level.
Usage
There are two ways to use the type checker: run it from the command line, or integrate it into your editor’s diagnostics via the language server.
The Command Line
From your workspace’s root directory, run the following:
$ solargraph typecheck
For problems in a particular file:
$ solargraph ./path/to/file.rb
Use the --level
option to select a check level, e.g., solargraph typecheck --level strict
.
Diagnostics
Editors that support diagnostics can use the typecheck reporter. Add typecheck
to your workspace’s reporters in .solargraph.yml
:
reporters:
- typecheck
You can also specify a level:
reporters:
- typecheck:strict
Ignoring Errors
The typechecker can be told to ignore errors with the @sg-ignore
directive:
# @sg-ignore
UnknownConstant1 # not reported
UnknownConstant2 # reported
You can also use @sg-ignore
to suppress type inference discrepancies in method tags. Example in typed
mode:
# Ignore the discrepancy between tagged type `String` and inferred type `Integer`
# @sg-ignore
# @return [String]
def foo
100
end
Type Checking Rules
Solargraph supports four levels of type checking: normal, typed, strict, and strong. Each level enforces increasingly stringent type safety.
The check levels are designed to facilitate gradual typing. The more you make use of YARD documentation, the easier it becomes to increase the check level you use to verify your project’s code.
Common Rules
A few rules are standard across all check levels, including tag type validation and method call arity.
Tag Type Validation
The checker verifies that all tag types, e.g., @return [String]
, can be resolved. Classes, modules, duck types (e.g., [#to_s]
), and special YARD types like [Boolean]
and [void]
are all valid.
Method Call Arity
Method calls get checked to ensure that their arguments match the method’s defined parameters.
# Arity check example
def example(arg1, arg2 = 0); end
example(1) # OK
example(1, 2) # OK
example(1, 2, 3) # Error: Too many arguments
Check Levels
The following sections describe the rules that apply to each check level.
The Normal Level
@return
and@param
tags are optional- Validate arity of calls to explicit methods
- Resolve namespaces in all type tags
- Ignore all undefined types
Normal checks validate YARD type tags, but do not perform type inference on the code. Untagged types are ignored.
class NormalExample
# OK: Return type is a recognized class
#
# @return [String]
def method1; end
# Error: NotARealClass could not be resolved
#
# @return [NotARealClass]
def method2; end
# OK: Method without `@return` tag is ignored
def method3; end
end
The Typed Level
@return
and@param
tags are optional- Validate arity of calls to explicit methods
- Resolve namespaces in all type tags
- Validate existing
@return
tags against inferred types - Validate existing
@type
tags against inferred types - Validate existing
@param
tags against arguments - Loose
@return
tag matches - Ignore all undefined types
Typed checks compare type tags to inferred types.
class TypedExample
# OK: Return type matches inferred type
#
# @return [String]
def method1
'a string'
end
# Error: Tagged type Integer does not match inferred type String
#
# @return [Integer]
def method2
'a string'
end
# OK: Method without `@return` tag is ignored
#
def method3
'a string'
end
# OK: Tagged type is valid when inferred type is unknown
#
# @return [String]
def method4
unknown_method
end
end
The Strict Level
@param
tags are optional- Validate arity of calls to explicit methods
- All methods must have either a
@return
tag or an inferred type - Resolve namespaces in all type tags
- Validate existing
@return
tags against inferred types - Validate existing
@param
tags against arguments - Validate existing
@type
tags against inferred types - Strict
@return
tag matches - Validate method calls
- Ignore undefined types from external sources
Strict checks require all methods to have either a tagged type or an inferred type. If an untagged method’s type cannot be inferred, the type checker issues a warning. @param
tags are optional, but checked if they exist.
class StrictExample
# OK: Type checker knows this method returns a String
#
def method1
'a string'
end
# OK: Tagged type matches inferred type
#
# @return [String]
def method2
'a string'
end
# Error: Method's type could not be inferred
#
def method3
unknown_method
end
end
Attributes are required to have @return
tags because their types cannot be inferred from code.
class StrictExample
# OK: Attribute has a `@return` tag
#
# @return [String]
attr_reader :attr1
# Error: Untagged attribute cannot be inferred
#
attr_reader :attr2
end
If a method has @param
tags, the type checker validates it arguments.
class StrictExample
# @param arg1 [Integer]
# @return [String]
def method1(arg1)
arg1.to_s
end
end
# OK: Method call has valid argument
StrictExample.new.method1(100)
# Error: arg1 expected Integer, received String
StrictExample.new.method1('100')
The Strong Level
@return
and@param
tags are required- Validate arity of calls to explicit methods
- Resolve namespaces in all type tags
- Validate
@return
tags against inferred types - Validate
@param
tags against arguments - Validate existing
@type
tags against inferred types - Strict
@return
tag matches - Validate method calls
- Ignore undefined types from external sources
Strong checks require all method types to be tagged and the inferred types to match the tags. @param
tags are required.
class StrongExample
# OK: Parameter and return types are valid.
#
# @param arg1 [Integer]
# @return [String]
def method1(arg1)
arg1.to_s
end
# Error: Parameter type is untagged.
#
# @return [String]
def method2(arg1)
"received #{arg1}"
end
end
# OK: Method call is correct
StrongExample.new.method1(100)
# Error: arg1 expected Integer, received String
StrongExample.new.method1('100')
Undefined Types
At the normal and typed check levels, the type checker allows undefined types. This behavior is similar to the way TypeScript treats Any
.
At the strict and strong check levels, a call to an undefined return type results in an “unresolved call” error.
class Example
def method1; end
end
Example.new.method1.method2
# Normal type checks ignore the call to `method2` because the return type of
# `Example#method1` is untagged.
#
# Strict type checks report an error because `Example#method1` is inferred to
# be `nil` and `NilClass#method2` is not a recognized method.
Overloaded Methods
Overloaded methods inherit tags from superclasses or included modules.
module Mixin
# @return [Integer]
def integer
100
end
end
class User1
include Mixin
# OK: User1#integer returns same type as Mixin#integer
#
def integer
200
end
end
class User2
include Mixin
# Error: Declared return type Integer does not match inferred type String
#
def integer
'100'
end
end
Abstract Methods
Sometimes you might want to declare “abstract” methods. One example is a class with methods that are declared as part of its interface but need to be implemented in subclasses. Here’s a simple example of a class with an abstract method:
class Greeter
# Subclasses should implement this method
#
# @return [String]
def greeting
raise NotImplementedError
end
# @return [void]
def say_greeting
puts greeting
end
end
class Person < Greeter
def greeting
"Hello!"
end
end
class Cowboy < Greeter
def greeting
"Howdy!"
end
end
class Stranger < Greeter
end
Person.new.say_greeting #=> "Hello!"
Cowboy.new.say_greeting #=> "Howdy!"
Stranger.new.say_greeting #=> NotImplementedError
In strict mode, type checks would report an error for Greeter#greeting
because it doesn’t return a String
. You can use the @abstract
tag to indicate that its return type should be applied to subclasses that implement it.
class Greeter
# @abstract
# @return [String]
def greeting
raise NotImplementedError
end
end
class Right < Greeter
# OK: The implementation satisfies the abstract method's expectations
#
def greeting
"hello"
end
end
class Wrong < Greeter
# Error: Declared return type String does not match inferred type Integer
#
def greeting
100
end
end