Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/inline.md
Original file line number Diff line number Diff line change
Expand Up @@ -632,3 +632,18 @@ class Calculator
end
end
```

## Loading Ruby Files with the `rbs` Command

The `--inline` option of the `rbs` command loads inline RBS declarations from Ruby files, like `-I` loads RBS files from a directory.
When a directory is given, all `.rb` files under the directory are loaded.
When a file is given, the file is loaded regardless of its extension.

```console
$ rbs -I sig --inline lib validate
```

The option is available for the commands that load the RBS environment, like `validate`, `methods`, `ancestors`, and `paths`.

`rbs validate` validates the inline declarations as well as the RBS files.
The Ruby code that cannot be imported as inline declarations, like a class definition with non-constant super class, is reported as warnings, and doesn't make the command fail.
14 changes: 14 additions & 0 deletions lib/rbs/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ class LibraryOptions
attr_reader :repos
attr_reader :libs
attr_reader :dirs
attr_reader :inline_dirs

def initialize()
@core_root = EnvironmentLoader::DEFAULT_CORE_ROOT
@repos = []

@libs = []
@dirs = []
@inline_dirs = []
@config_path = Collection::Config.find_config_path || Collection::Config::PATH
end

Expand All @@ -46,6 +48,10 @@ def loader
loader.add(path: Pathname(dir))
end

inline_dirs.each do |dir|
loader.add(inline: Pathname(dir))
end

libs.each do |lib|
name, version = lib.split(/:/, 2)
next unless name
Expand All @@ -64,6 +70,10 @@ def setup_library_options(opts)
dirs << dir
end

opts.on("--inline DIR", "Load inline RBS declarations from Ruby files in the directory") do |dir|
inline_dirs << dir
end

opts.on("--no-stdlib", "Skip loading standard library signatures") do
self.core_root = nil
end
Expand Down Expand Up @@ -561,6 +571,10 @@ def run_paths(args, options)
end
end

loader.inline_dirs.each do |dir|
stdout.puts "#{dir} (#{kind_of[dir]}, inline)"
end

0
end

Expand Down
54 changes: 43 additions & 11 deletions lib/rbs/cli/validate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,13 @@ def initialize(args:, options:)

Validate RBS files. It ensures the type names in RBS files are present and the type applications have correct arity.

Inline RBS declarations in Ruby files given by `--inline` are validated too.
Ruby code that cannot be imported as inline declarations is reported as warnings.

Examples:

$ rbs validate
$ rbs -I sig --inline lib validate
EOU

opts.on("--silent", "This option has been deprecated and does nothing.") do
Expand All @@ -77,6 +81,8 @@ def initialize(args:, options:)
end

def run
report_inline_diagnostics

@errors.try do
validate_class_module_definition
validate_class_module_alias_definition
Expand All @@ -89,6 +95,14 @@ def run

private

def report_inline_diagnostics
@env.each_ruby_source do |source|
source.diagnostics.each do |diagnostic|
RBS.logger.warn "#{diagnostic.location}: #{diagnostic.message} (#{diagnostic.class.name})"
end
end
end

def validate_class_module_definition
@env.class_decls.each do |name, entry|
RBS.logger.info "Validating class/module definition: `#{name}`..."
Expand All @@ -107,7 +121,14 @@ def validate_class_module_definition
when Environment::ClassEntry
entry.each_decl do |decl|
if super_class = decl.super_class
super_class.args.each do |arg|
args =
case super_class
when AST::Declarations::Class::Super
super_class.args
when AST::Ruby::Declarations::ClassDecl::SuperClass
super_class.type_args
end
args.each do |arg|
@validator.validate_type(arg, context: nil)
end
end
Expand Down Expand Up @@ -138,7 +159,7 @@ def validate_class_module_definition
@validator.validate_type_params(
d.type_params,
type_name: name,
location: d.location&.aref(:type_params)
location: d.is_a?(AST::Declarations::Base) ? d.location&.aref(:type_params) : nil
)

d.type_params.each do |param|
Expand All @@ -165,19 +186,20 @@ def validate_class_module_definition
when AST::Members::MethodDefinition
@validator.validate_method_definition(member, type_name: name)
when AST::Members::Mixin
params =
if member.name.class?
module_decl = @env.module_entry(member.name, normalized: true) or raise
module_decl.type_params
else
interface_decl = @env.interface_decls.fetch(member.name)
interface_decl.decl.type_params
end
InvalidTypeApplicationError.check!(type_name: member.name, params: params, args: member.args, location: member.location)
InvalidTypeApplicationError.check!(type_name: member.name, params: mixin_type_params(member.name), args: member.args, location: member.location)
when AST::Members::Var
@validator.validate_variable(member)
end
end
when AST::Ruby::Declarations::Base
decl.members.each do |member|
case member
when AST::Ruby::Members::DefMember
@validator.validate_method_definition(member, type_name: name)
when AST::Ruby::Members::MixinMember
InvalidTypeApplicationError.check!(type_name: member.module_name, params: mixin_type_params(member.module_name), args: member.type_args, location: member.location)
end
end
else
raise "Unknown declaration: #{decl.class}"
end
Expand All @@ -187,6 +209,16 @@ def validate_class_module_definition
end
end

def mixin_type_params(name)
if name.class?
module_decl = @env.module_entry(name, normalized: true) or raise
module_decl.type_params
else
interface_decl = @env.interface_decls.fetch(name)
interface_decl.decl.type_params
end
end

def validate_class_module_alias_definition
@env.class_alias_decls.each do |name, entry|
RBS.logger.info "Validating class/module alias definition: `#{name}`..."
Expand Down
28 changes: 27 additions & 1 deletion lib/rbs/environment_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Library < Struct.new(:name, :version, keyword_init: true)

attr_reader :libs
attr_reader :dirs
attr_reader :inline_dirs

DEFAULT_CORE_ROOT = Pathname(_ = __dir__) + "../../core"

Expand All @@ -43,12 +44,15 @@ def initialize(core_root: DEFAULT_CORE_ROOT, repository: Repository.new)

@libs = Set.new
@dirs = []
@inline_dirs = []
end

def add(path: nil, library: nil, version: nil, resolve_dependencies: true)
def add(path: nil, library: nil, version: nil, resolve_dependencies: true, inline: nil)
case
when path
dirs << path
when inline
inline_dirs << inline
when library
if libs.add?(Library.new(name: library, version: version)) && resolve_dependencies
resolve_dependencies(library: library, version: version)
Expand Down Expand Up @@ -119,6 +123,10 @@ def load(env:)
env.add_source(Source::RBS.new(buffer, dirs, decls))
end

each_inline_source do |source|
env.add_source(source)
end

loaded
end

Expand Down Expand Up @@ -163,5 +171,23 @@ def each_signature
end
end
end

def each_inline_source
files = Set[]

inline_dirs.each do |dir|
FileFinder.each_file(dir, skip_hidden: false, extension: "rb") do |path|
next if files.include?(path)

files << path
content = path.read(encoding: "UTF-8")
buffer = Buffer.new(name: path, content: content)
prism = Prism.parse(content, filepath: path.to_s)
result = InlineParser.parse(buffer, prism)

yield Source::Ruby.new(buffer, prism, result.declarations, result.diagnostics)
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/rbs/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def detailed_message(highlight: false, **)
io.puts msg
io.puts
io.print "\e[1m" if highlight
io.puts " #{location.buffer.lines[location.end_line - 1]}"
io.puts " #{location.buffer.top_buffer.lines[location.end_line - 1]}"
io.puts " #{indent}#{marker}"
io.print "\e[m" if highlight
io.string
Expand Down
6 changes: 3 additions & 3 deletions lib/rbs/file_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ module RBS
module FileFinder
module_function

def self.each_file(path, immediate: nil, skip_hidden:, &block)
return enum_for((__method__ or raise), path, immediate: immediate, skip_hidden: skip_hidden) unless block
def self.each_file(path, immediate: nil, skip_hidden:, extension: "rbs", &block)
return enum_for((__method__ or raise), path, immediate: immediate, skip_hidden: skip_hidden, extension: extension) unless block

case
when path.file?
yield path

when path.directory?
paths = path.glob("**/*.rbs")
paths = path.glob("**/*.#{extension}")

if skip_hidden
paths.select! do |child|
Expand Down
9 changes: 8 additions & 1 deletion lib/rbs/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,14 @@ def validate_variable(var)
def validate_class_alias(entry:)
case env.normalize_module_name?(entry.decl.new_name)
when nil
raise NoTypeFoundError.new(type_name: entry.decl.old_name, location: entry.decl.location&.[](:old_name))
location =
case decl = entry.decl
when AST::Ruby::Declarations::ClassModuleAliasDecl
decl.location
else
decl.location&.[](:old_name)
end
raise NoTypeFoundError.new(type_name: entry.decl.old_name, location: location)
when false
raise CyclicClassAliasDefinitionError.new(entry)
end
Expand Down
1 change: 1 addition & 0 deletions sig/cli.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module RBS

attr_reader libs: Array[String]
attr_reader dirs: Array[String]
attr_reader inline_dirs: Array[String]
attr_reader repos: Array[String]

def initialize: () -> void
Expand Down
4 changes: 4 additions & 0 deletions sig/cli/validate.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ module RBS

private

def report_inline_diagnostics: () -> void

def validate_class_module_definition: () -> void
def mixin_type_params: (TypeName) -> Array[AST::TypeParam]

def validate_class_module_alias_definition: () -> void
def validate_interface: () -> void
def validate_constant: () -> void
Expand Down
12 changes: 12 additions & 0 deletions sig/environment_loader.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ module RBS
attr_reader libs: Set[Library]
attr_reader dirs: Array[Pathname]

# Paths to load inline RBS declarations from Ruby files.
attr_reader inline_dirs: Array[Pathname]

# The source where the RBS comes from.
#
# `:core` means it is part of core library.
Expand Down Expand Up @@ -74,12 +77,17 @@ module RBS
# All `.rbs` files from the given directory will be loaded.
# Specifying a file will load the file regardless the extension of the file is.
#
# `inline` can be a file or a directory.
# All `.rb` files from the given directory will be parsed and the inline RBS declarations in them will be loaded.
# Specifying a file will load the file regardless the extension of the file is.
#
# `library` can be a name of a gem.
# Specifying `nil` to `version` will load any version available.
# It first tries to load RBS files from gem with specified version.
# If RBS files cannot be found in the gem, it tries to load RBSs from repository.
#
def add: (path: Pathname) -> void
| (inline: Pathname) -> void
| (library: String, version: String?, ?resolve_dependencies: boolish) -> void

def resolve_dependencies: (library: String, version: String?) -> void
Expand All @@ -96,6 +104,7 @@ module RBS
# Raises `UnknownLibraryError` if RBS cannot be loaded from a library.
#
# Returns an array of tuples of the declaration, path to the file, and the source.
# Declarations loaded from Ruby files (inline RBS declarations) are not included in the array.
#
def load: (env: Environment) -> Array[[AST::Declarations::t, Pathname, source]]

Expand All @@ -107,5 +116,8 @@ module RBS
def each_signature: () { (source, Pathname, Buffer, Array[AST::Declarations::t], Array[AST::Directives::t]) -> void } -> void

def each_dir: { (source, Pathname) -> void } -> void

# Yields `Source::Ruby` for each Ruby file under `inline_dirs`
def each_inline_source: () { (Source::Ruby) -> void } -> void
end
end
10 changes: 8 additions & 2 deletions sig/file_finder.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@ module RBS
# FileFinder.each_file(Pathname("_hidden"), skip_hidden: true) {} # => yields all `.rbs` files under `_hidden`, skips other `_` directories
# ```
#
# `extension` is the extension of files to enumerate under a directory, without the leading dot. The default is `"rbs"`.
#
# ```rb
# FileFinder.each_file(Pathname("lib"), skip_hidden: false, extension: "rb") {} # => yields all `.rb` files under `lib`
# ```
#
# `immediate` keyword is unused and left for API compatibility.
#
def self?.each_file: (Pathname path, ?immediate: top, skip_hidden: boolish) { (Pathname) -> void } -> void
| (Pathname path, ?immediate: top, skip_hidden: boolish) -> Enumerator[Pathname, void]
def self?.each_file: (Pathname path, ?immediate: top, skip_hidden: boolish, ?extension: String) { (Pathname) -> void } -> void
| (Pathname path, ?immediate: top, skip_hidden: boolish, ?extension: String) -> Enumerator[Pathname, void]
end
end
2 changes: 1 addition & 1 deletion sig/validator.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ module RBS

# Validates the type parameters in generic methods.
#
def validate_method_definition: (AST::Members::MethodDefinition, type_name: TypeName) -> void
def validate_method_definition: (AST::Members::MethodDefinition | AST::Ruby::Members::DefMember, type_name: TypeName) -> void

# Validates the type parameters if there is no circular dependencies between the bounds.
#
Expand Down
Loading
Loading