diff --git a/Rakefile b/Rakefile index b9c9340ef..b6bdd5099 100644 --- a/Rakefile +++ b/Rakefile @@ -17,6 +17,12 @@ test_config = lambda do |t| t.test_files = FileList["test/**/*_test.rb"].reject do |path| path =~ %r{test/stdlib/} end + if defined?(RubyMemcheck) + if t.is_a?(RubyMemcheck::TestTask) + t.verbose = true + t.options = '-v' + end + end end Rake::TestTask.new(test: :compile, &test_config) @@ -225,13 +231,15 @@ task :stdlib_test => :compile do end task :typecheck_test => :compile do - FileList["test/typecheck/*"].each do |test| - Dir.chdir(test) do - expectations = File.join(test, "steep_expectations.yml") - if File.exist?(expectations) - sh "steep check --with_expectations" - else - sh "steep check" + Bundler.with_unbundled_env do + FileList["test/typecheck/*"].each do |test| + Dir.chdir(test) do + expectations = File.join(test, "steep_expectations.yml") + if File.exist?(expectations) + sh "#{__dir__}/bin/steep check --with_expectations" + else + sh "#{__dir__}/bin/steep check" + end end end end @@ -535,4 +543,4 @@ task :prepare_profiling do Rake::Task[:"clobber"].invoke Rake::Task[:"templates"].invoke Rake::Task[:"compile"].invoke -end \ No newline at end of file +end diff --git a/docs/aliases.md b/docs/aliases.md new file mode 100644 index 000000000..0004da331 --- /dev/null +++ b/docs/aliases.md @@ -0,0 +1,79 @@ +# Aliases + +This document explains module/class aliases and type aliases. + +## Module/class alias + +Module/class aliases give another name to a module/class. +This is useful for some syntaxes that has lexical constraints. + +```rbs +class C +end + +class D = C # ::D is an alias for ::C + +class E < D # ::E inherits from ::D, which is actually ::C +end +``` + +Note that module/class aliases cannot be recursive. + +So, we can define a *normalization* of aliased module/class names. +Normalization follows the chain of alias definitions and resolves them to the original module/class defined with `module`/`class` syntax. + +```rbs +class C +end + +class D = C +class E = D +``` + +`::E` is defined as an alias, and it can be normalized to `::C`. + +## Type alias + +The biggest difference from module/class alias is that type alias can be recursive. + +```rbs +# cons_cell type is defined recursively +type cons_cell = nil + | [Integer, cons_cell] +``` + +This means type aliases *cannot be* normalized generally. +So, we provide another operation for type alias, `DefinitionBuilder#expand_alias` and its family. +It substitutes with the immediate right hand side of a type alias. + +``` +cons_cell ===> nil | [Integer, cons_cell] (expand 1 step) + ===> nil | [Integer, nil | [Integer, cons_cell]] (expand 2 steps) + ===> ... (expand will go infinitely) +``` + +Note that the namespace of a type alias *can be* normalized, because they are module names. + +```rbs +module M + type t = String +end + +module N = M +``` + +With the type definition above, a type `::N::t` can be normalized to `::M::t`. +And then it can be expanded to `::String`. + +> [!NOTE] +> This is something like an *unfold* operation in type theory. + +## Type name resolution + +Type name resolution in RBS usually rewrites *relative* type names to *absolute* type names. +`Environment#resolve_type_names` converts all type names in the RBS type definitions, and returns a new `Environment` object. + +It also *normalizes* modules names in type names. + +- If the type name can be resolved and normalized successfully, the AST has *absolute* type names. +- If the type name resolution/normalization fails, the AST has *relative* type names. diff --git a/lib/rbs/cli/validate.rb b/lib/rbs/cli/validate.rb index b4ba30897..e44279d96 100644 --- a/lib/rbs/cli/validate.rb +++ b/lib/rbs/cli/validate.rb @@ -131,7 +131,7 @@ def validate_class_module_definition self_params = if self_type.name.class? - @env.normalized_module_entry(self_type.name)&.type_params + @env.module_entry(self_type.name, normalized: true)&.type_params else @env.interface_decls[self_type.name]&.decl&.type_params end @@ -188,7 +188,7 @@ def validate_class_module_definition end params = if member.name.class? - module_decl = @env.normalized_module_entry(member.name) or raise + module_decl = @env.module_entry(member.name, normalized: true) or raise module_decl.type_params else interface_decl = @env.interface_decls.fetch(member.name) diff --git a/lib/rbs/definition_builder/ancestor_builder.rb b/lib/rbs/definition_builder/ancestor_builder.rb index 0b513779b..5c9a111d6 100644 --- a/lib/rbs/definition_builder/ancestor_builder.rb +++ b/lib/rbs/definition_builder/ancestor_builder.rb @@ -220,7 +220,7 @@ def one_instance_ancestors(type_name) InvalidTypeApplicationError.check2!(type_name: super_class.name, args: super_class.args, env: env, location: super_class.location) end - super_entry = env.normalized_class_entry(super_name) or raise + super_entry = env.class_entry(super_name, normalized: true) or raise super_args = AST::TypeParam.normalize_args(super_entry.type_params, super_args) ancestors = OneAncestors.class_instance( @@ -248,7 +248,7 @@ def one_instance_ancestors(type_name) module_name = module_self.name if module_name.class? - module_entry = env.normalized_module_class_entry(module_name) or raise + module_entry = env.module_class_entry(module_name, normalized: true) or raise module_name = module_entry.name self_args = AST::TypeParam.normalize_args(module_entry.type_params, module_self.args) end @@ -359,7 +359,7 @@ def mixin_ancestors0(decl, type_name, align_params:, included_modules:, included MixinClassError.check!(type_name: type_name, env: env, member: member) NoMixinFoundError.check!(member.name, env: env, member: member) - module_decl = env.normalized_module_entry(module_name) or raise + module_decl = env.module_entry(module_name, normalized: true) or raise module_args = AST::TypeParam.normalize_args(module_decl.type_params, module_args) module_name = env.normalize_module_name(module_name) @@ -378,7 +378,7 @@ def mixin_ancestors0(decl, type_name, align_params:, included_modules:, included MixinClassError.check!(type_name: type_name, env: env, member: member) NoMixinFoundError.check!(member.name, env: env, member: member) - module_decl = env.normalized_module_entry(member.name) or raise + module_decl = env.module_entry(member.name, normalized: true) or raise module_name = module_decl.name module_args = member.args.map {|type| align_params ? type.sub(align_params) : type } @@ -396,7 +396,7 @@ def mixin_ancestors0(decl, type_name, align_params:, included_modules:, included MixinClassError.check!(type_name: type_name, env: env, member: member) NoMixinFoundError.check!(member.name, env: env, member: member) - module_decl = env.normalized_module_entry(module_name) or raise + module_decl = env.module_entry(module_name, normalized: true) or raise module_args = AST::TypeParam.normalize_args(module_decl.type_params, module_args) module_name = env.normalize_module_name(module_name) diff --git a/lib/rbs/environment.rb b/lib/rbs/environment.rb index 03c27ba4c..ffbdfaf69 100644 --- a/lib/rbs/environment.rb +++ b/lib/rbs/environment.rb @@ -223,21 +223,17 @@ def class_alias?(name) end end - def class_entry(type_name) - case - when (class_entry = class_decls[type_name]).is_a?(ClassEntry) - class_entry - when (class_alias = class_alias_decls[type_name]).is_a?(ClassAliasEntry) - class_alias + def class_entry(type_name, normalized: false) + case entry = constant_entry(type_name, normalized: normalized || false) + when ClassEntry, ClassAliasEntry + entry end end - def module_entry(type_name) - case - when (module_entry = class_decls[type_name]).is_a?(ModuleEntry) - module_entry - when (module_alias = class_alias_decls[type_name]).is_a?(ModuleAliasEntry) - module_alias + def module_entry(type_name, normalized: false) + case entry = constant_entry(type_name, normalized: normalized || false) + when ModuleEntry, ModuleAliasEntry + entry end end @@ -253,26 +249,40 @@ def normalized_class_entry(type_name) end def normalized_module_entry(type_name) - if name = normalize_module_name?(type_name) - case entry = module_entry(name) - when ModuleEntry, nil - entry - when ModuleAliasEntry - raise - end - end + module_entry(type_name, normalized: true) end - def module_class_entry(type_name) - class_entry(type_name) || module_entry(type_name) + def module_class_entry(type_name, normalized: false) + entry = constant_entry(type_name, normalized: normalized || false) + if entry.is_a?(ConstantEntry) + nil + else + entry + end end def normalized_module_class_entry(type_name) - normalized_class_entry(type_name) || normalized_module_entry(type_name) + module_class_entry(type_name, normalized: true) end - def constant_entry(type_name) - class_entry(type_name) || module_entry(type_name) || constant_decls[type_name] + def constant_entry(type_name, normalized: false) + if normalized + if normalized_name = normalize_module_name?(type_name) + class_decls.fetch(normalized_name, nil) + else + # The type_name may be declared with constant declaration + unless type_name.namespace.empty? + parent = type_name.namespace.to_type_name + normalized_parent = normalize_module_name?(parent) or return + constant_name = TypeName.new(name: type_name.name, namespace: normalized_parent.to_namespace) + constant_decls.fetch(constant_name, nil) + end + end + else + class_decls.fetch(type_name, nil) || + class_alias_decls.fetch(type_name, nil) || + constant_decls.fetch(type_name, nil) + end end def normalize_type_name?(name) @@ -307,6 +317,10 @@ def normalize_type_name!(name) end end + def normalize_type_name(name) + normalize_type_name?(name) || name + end + def normalized_type_name?(type_name) case when type_name.interface? @@ -321,53 +335,44 @@ def normalized_type_name?(type_name) end def normalized_type_name!(name) - normalized_type_name?(name) or raise "Normalized type name is expected but given `#{name}`, which is normalized to `#{normalize_type_name?(name)}`" + normalized_type_name?(name) or raise "Normalized type name is expected but given `#{name}`" name end - def normalize_type_name(name) - normalize_type_name?(name) || name - end - - def normalize_module_name(name) - normalize_module_name?(name) or name - end - def normalize_module_name?(name) raise "Class/module name is expected: #{name}" unless name.class? name = name.absolute! unless name.absolute? - if @normalize_module_name_cache.key?(name) - return @normalize_module_name_cache[name] + original_name = name + + if @normalize_module_name_cache.key?(original_name) + return @normalize_module_name_cache[original_name] end - unless name.namespace.empty? - parent = name.namespace.to_type_name - if normalized_parent = normalize_module_name?(parent) - type_name = TypeName.new(namespace: normalized_parent.to_namespace, name: name.name) - else - @normalize_module_name_cache[name] = nil - return + if alias_entry = class_alias_decls.fetch(name, nil) + unless alias_entry.decl.old_name.absolute? + # Having relative old_name means the type name resolution was failed. + # Run TypeNameResolver for failure reason + resolver = Resolver::TypeNameResolver.build(self) + name = resolver.resolve_namespace(name, context: nil) + @normalize_module_name_cache[original_name] = name + return name end - else - type_name = name - end - @normalize_module_name_cache[name] = false + name = alias_entry.decl.old_name + end - entry = constant_entry(type_name) + if class_decls.key?(name) + @normalize_module_name_cache[original_name] = name + end + end - normalized_type_name = - case entry - when ClassEntry, ModuleEntry - type_name - when ClassAliasEntry, ModuleAliasEntry - normalize_module_name?(entry.decl.old_name) - else - nil - end + def normalize_module_name(name) + normalize_module_name?(name) || name + end - @normalize_module_name_cache[name] = normalized_type_name + def normalize_module_name!(name) + normalize_module_name?(name) or raise "Module name `#{name}` cannot be normalized" end def insert_decl(decl, outer:, namespace:) @@ -509,7 +514,7 @@ def resolve_signature(resolver, table, dirs, decls, only: nil) end def resolve_type_names(only: nil) - resolver = Resolver::TypeNameResolver.new(self) + resolver = Resolver::TypeNameResolver.build(self) env = Environment.new table = UseMap::Table.new() diff --git a/lib/rbs/errors.rb b/lib/rbs/errors.rb index 88ceb69f8..2f21c0940 100644 --- a/lib/rbs/errors.rb +++ b/lib/rbs/errors.rb @@ -93,7 +93,7 @@ def self.check2!(env:, type_name:, args:, location:) params = case when type_name.class? - decl = env.normalized_module_class_entry(type_name) or raise + decl = env.module_class_entry(type_name, normalized: true) or raise decl.type_params when type_name.interface? env.interface_decls.fetch(type_name).decl.type_params diff --git a/lib/rbs/resolver/constant_resolver.rb b/lib/rbs/resolver/constant_resolver.rb index 2106df218..7bd0f1248 100644 --- a/lib/rbs/resolver/constant_resolver.rb +++ b/lib/rbs/resolver/constant_resolver.rb @@ -32,7 +32,7 @@ def initialize(environment) end environment.class_alias_decls.each do |name, entry| - normalized_entry = environment.normalized_module_class_entry(name) or next + normalized_entry = environment.module_class_entry(name, normalized: true) or next constant = constant_of_module(name, normalized_entry) # Insert class/module aliases into `children_table` and `toplevel` table @@ -176,7 +176,7 @@ def constants_from_context(context, constants:) end def constants_from_ancestors(module_name, constants:) - entry = builder.env.normalized_module_class_entry(module_name) or raise + entry = builder.env.module_class_entry(module_name, normalized: true) or raise if entry.is_a?(Environment::ClassEntry) || entry.is_a?(Environment::ModuleEntry) constants.merge!(table.children(BuiltinNames::Object.name) || raise) diff --git a/lib/rbs/resolver/type_name_resolver.rb b/lib/rbs/resolver/type_name_resolver.rb index 8b71f4b2e..a0c966ca8 100644 --- a/lib/rbs/resolver/type_name_resolver.rb +++ b/lib/rbs/resolver/type_name_resolver.rb @@ -5,17 +5,36 @@ module Resolver class TypeNameResolver attr_reader :all_names attr_reader :cache - attr_reader :env + attr_reader :aliases - def initialize(env) - @all_names = Set[] + def initialize(all_names, aliases) + @all_names = all_names + @aliases = aliases @cache = {} - @env = env + end + + def self.new(*args) + if args.size == 1 && args[0].is_a?(Environment) + build(args[0]) + else + super + end + end + + def self.build(env) + all_names = Set.new #: Set[TypeName] all_names.merge(env.class_decls.keys) all_names.merge(env.interface_decls.keys) all_names.merge(env.type_alias_decls.keys) - all_names.merge(env.class_alias_decls.keys) + + aliases = {} #: Hash[TypeName, [TypeName, context]] + + env.class_alias_decls.each do |name, entry| + aliases[name] = [entry.decl.old_name, entry.context] + end + + new(all_names, aliases) end def try_cache(query) @@ -26,64 +45,131 @@ def try_cache(query) end def resolve(type_name, context:) - if type_name.absolute? + if type_name.absolute? && has_type_name?(type_name) return type_name end try_cache([type_name, context]) do - head, tail = partition(type_name) + if type_name.class? + resolve_namespace0(type_name, context, Set.new) || nil + else + namespace = type_name.namespace - head = resolve_in(head, context) - - if head - if tail - absolute_name = tail.with_prefix(head.to_namespace) - if env.normalize_type_name?(absolute_name) - absolute_name - end + if namespace.empty? + resolve_type_name(type_name.name, context) else - head + if namespace = resolve_namespace0(namespace.to_type_name, context, Set.new) + type_name = TypeName.new(name: type_name.name, namespace: namespace.to_namespace) + has_type_name?(type_name) + end end end end end - def partition(type_name) - if type_name.namespace.empty? - head = type_name.name - tail = nil - else - head, *tail = type_name.namespace.path + def resolve_namespace(type_name, context:) + if type_name.absolute? && has_type_name?(type_name) + return type_name + end - head or raise + unless type_name.class? + raise "Type name must be a class name: #{type_name}" + end + + try_cache([type_name, context]) do + ns = resolve_namespace0(type_name, context, Set.new) or return ns + end + end - tail = TypeName.new( - name: type_name.name, - namespace: Namespace.new(absolute: false, path: tail) - ) + def has_type_name?(full_name) + if all_names.include?(full_name) + full_name end + end - [head, tail] + def aliased_name?(type_name) + if aliases.key?(type_name) + type_name + end end - def resolve_in(head, context) + def resolve_type_name(type_name, context) if context - parent, child = context - case child + outer, inner = context + case inner when false - resolve_in(head, parent) + resolve_type_name(type_name, outer) + else + has_type_name?(inner) or raise "Context must be normalized: #{inner.inspect}" + has_type_name?(TypeName.new(name: type_name, namespace: inner.to_namespace)) || resolve_type_name(type_name, outer) + end + else + type_name = TypeName.new(name: type_name, namespace: Namespace.root) + has_type_name?(type_name) + end + end + + def resolve_head_namespace(head, context) + if context + outer, inner = context + case inner + when false + resolve_head_namespace(head, outer) when TypeName - name = TypeName.new(name: head, namespace: child.to_namespace) - has_name?(name) || resolve_in(head, parent) + has_type_name?(inner) or raise "Context must be normalized: #{inner.inspect}" + type_name = TypeName.new(name: head, namespace: inner.to_namespace) + has_type_name?(type_name) || aliased_name?(type_name) || resolve_head_namespace(head, outer) end else - has_name?(TypeName.new(name: head, namespace: Namespace.root)) + type_name = TypeName.new(name: head, namespace: Namespace.root) + has_type_name?(type_name) || aliased_name?(type_name) end end - def has_name?(full_name) - if all_names.include?(full_name) - full_name + def normalize_namespace(type_name, rhs, context, visited) + if visited.include?(type_name) + # Cycle detected + return false + end + + visited << type_name + + begin + resolve_namespace0(rhs, context, visited) + ensure + visited.delete(type_name) + end + end + + def resolve_namespace0(type_name, context, visited) + head, *tail = [*type_name.namespace.path, type_name.name] + + head = head #: Symbol + + head = + if type_name.absolute? + root_name = TypeName.new(name: head, namespace: Namespace.root) + has_type_name?(root_name) || aliased_name?(root_name) + else + resolve_head_namespace(head, context) + end + + if head + if (rhs, context = aliases.fetch(head, nil)) + head = normalize_namespace(head, rhs, context, visited) or return head + end + + tail.inject(head) do |namespace, name| + type_name = TypeName.new(name: name, namespace: namespace.to_namespace) + case + when has_type_name?(type_name) + type_name + when (rhs, context = aliases.fetch(type_name, nil)) + m = normalize_namespace(type_name, rhs, context, visited) or return m + else + return nil + end + end end end end diff --git a/sig/environment.rbs b/sig/environment.rbs index 95b2d1fd1..4ef78830f 100644 --- a/sig/environment.rbs +++ b/sig/environment.rbs @@ -190,22 +190,58 @@ module RBS # Returns true if the type name is a class alias def class_alias?: (TypeName) -> bool - def class_entry: (TypeName) -> (ClassEntry | ClassAliasEntry | nil) + # Returns the entry for the class name + # + # Returns `nil` when: + # + # * The type name is not found + # * The type name is not a class + # + # It normalizes when `normalized: true` is given -- returns `ClassEntry` or `nil`. + # + def class_entry: (TypeName, normalized: true) -> ClassEntry? + | (TypeName, ?normalized: bool) -> (ClassEntry | ClassAliasEntry | nil) # Returns ClassEntry if the class definition is found, normalized in case of alias # + %a{deprecated: Use `class_entry(type_name, normalized: true)` instead. } def normalized_class_entry: (TypeName) -> ClassEntry? - def module_entry: (TypeName) -> (ModuleEntry | ModuleAliasEntry | nil) + # Returns the entry for the module name + # + # Returns `nil` when: + # + # * The type name is not found + # * The type name is not a module + # + # It normalizes when `normalized: true` is given -- returns `ModuleEntry` or `nil`. + # + def module_entry: (TypeName, normalized: true) -> ModuleEntry? + | (TypeName, ?normalized: bool) -> (ModuleEntry | ModuleAliasEntry | nil) # Returns ModuleEntry if the module definition is found, normalized in case of alias # + %a{deprecated: Use `module_entry(type_name, normalized: true)` instead. } def normalized_module_entry: (TypeName) -> ModuleEntry? - def constant_entry: (TypeName) -> (ClassEntry | ClassAliasEntry | ModuleEntry | ModuleAliasEntry | ConstantEntry | nil) + # Returns the entry for the type name + # + # The entry is one of ClassEntry, ModuleEntry, ConstantEntry, ClassAliasEntry, or ModuleAliasEntry. + # When `normalized: true` is given, it returns normalized entry -- one of ClassEntry, ModuleEntry, or ConstantEntry. + # + # Returns `nil` if not found. + # + def constant_entry: (TypeName, normalized: true) -> (ClassEntry | ModuleEntry | ConstantEntry | nil) + | (TypeName, ?normalized: bool) -> (ClassEntry | ClassAliasEntry | ModuleEntry | ModuleAliasEntry | ConstantEntry | nil) - def module_class_entry: (TypeName) -> (ClassEntry | ClassAliasEntry | ModuleEntry | ModuleAliasEntry | nil) + # Returns ClassEntry or ModuleEntry if the class/module definition is found + # + # Normalizes when `normalized: true` is given. + # + def module_class_entry: (TypeName, normalized: true) -> (ClassEntry | ModuleEntry | nil) + | (TypeName, ?normalized: bool) -> (ClassEntry | ClassAliasEntry | ModuleEntry | ModuleAliasEntry | nil) + %a{deprecated: Use `module_class_entry(type_name, normalized: true)` instead. } def normalized_module_class_entry: (TypeName) -> (ClassEntry | ModuleEntry | nil) @normalize_module_name_cache: Hash[TypeName, TypeName | false | nil] @@ -216,26 +252,39 @@ module RBS # * Returns `nil` if the rhs name cannot be found # * Returns `false` if the name is cyclic # + # Note that this method assumes the declarations have resolved type names. + # def normalize_module_name?: (TypeName) -> (TypeName | nil | false) - # Returns the original module name that is defined with `module` declaration + # Returns the original module name that is defined with `module`/`class` declaration # - # * Raises an error if given type name is not module + # * Raises an error if given type name is not module/class # * Calls `#absolute!` for relative module names # * Returns the name itself if the name cannot be normalized to a class/module # + # Note that this method assumes the declarations have resolved type names. + # def normalize_module_name: (TypeName) -> TypeName + # Returns the original module name that is defined with `module`/`class` declaration + # + # * Raises an error if given type name is not a module/class + def normalize_module_name!: (TypeName) -> TypeName + # Returns a normalized module/class name or a type name with a normalized namespace # # * Calls `#absolute!` for relative module names # * Returns `nil` if the typename cannot be found # * Returns `false` if the name is cyclic # + # Note that this method assumes the declarations have resolved type names. + # def normalize_type_name?: (TypeName) -> (TypeName | nil | false) # Normalize the type name or raises an error # + # Note that this method assumes the declarations have resolved type names. + # def normalize_type_name!: (TypeName) -> TypeName # Returns a normalized module/class name or a type name with a normalized namespace @@ -243,6 +292,8 @@ module RBS # * Calls `#absolute!` for relative module names # * Returns the typename itself if the name cannot be normalized # + # Note that this method assumes the declarations have resolved type names. + # def normalize_type_name: (TypeName) -> TypeName # Returns `true` if given type name is normalized diff --git a/sig/resolver/type_name_resolver.rbs b/sig/resolver/type_name_resolver.rbs index 96d156ea9..6af9b7c1b 100644 --- a/sig/resolver/type_name_resolver.rbs +++ b/sig/resolver/type_name_resolver.rbs @@ -8,28 +8,59 @@ module RBS class TypeNameResolver type query = [TypeName, context] - def initialize: (Environment) -> void + def initialize: (Set[TypeName] all_names, Hash[TypeName, [TypeName, context]] aliases) -> void - # Translates given type name to absolute type name. + def self.new: %a{deprecated: Use `build` to build TypeNameResolver from Environment} (Environment) -> instance + | (Set[TypeName] all_names, Hash[TypeName, [TypeName, context]] aliases) -> instance + + def self.build: (Environment) -> instance + + # Translates given type name to absolute type name + # # Returns `nil` if cannot find associated type name. + # Module names in the type name are normalized. # def resolve: (TypeName, context: context) -> TypeName? - private + # Translates given type name to absolute type name + # + # Returns `false` if a cycle is detected while resolving aliases. + # Returns `nil` if the type name cannot be resolved. + # + def resolve_namespace: (TypeName, context: context) -> (TypeName | false | nil) - attr_reader env: Environment + private attr_reader all_names: Set[TypeName] + attr_reader aliases: Hash[TypeName, [TypeName, context]] + attr_reader cache: Hash[query, TypeName?] - def has_name?: (TypeName) -> TypeName? + # Returns the type name if it exists in `all_names` (normalized) + # + def has_type_name?: (TypeName) -> TypeName? + + # Returns the type name if it is an alias (not normalized) + # + def aliased_name?: (TypeName) -> TypeName? def try_cache: (query) { () -> TypeName? } -> TypeName? - def resolve_in: (Symbol, context) -> TypeName? + # Translates the head module name in the context and returns an absolute type name + # + # Returns `nil` if cannot find associated type name. + # The returned namespace may be an alias + # + def resolve_head_namespace: (Symbol, context) -> TypeName? + + # Resolves the type name in the given context + # + def resolve_type_name: (Symbol, context) -> TypeName? + + def resolve_namespace0: (TypeName, context, Set[TypeName]) -> (TypeName | false | nil) - def partition: (TypeName) -> [Symbol, TypeName?] + def normalize_namespace: (TypeName, TypeName rhs, context, Set[TypeName]) -> (TypeName | false | nil) end end end diff --git a/test/rbs/environment_test.rb b/test/rbs/environment_test.rb index 833b55e57..39c3514a5 100644 --- a/test/rbs/environment_test.rb +++ b/test/rbs/environment_test.rb @@ -528,6 +528,39 @@ module N = M::Baz assert_equal type_name("::Foo::Bar::Baz"), env.normalize_module_name(type_name("::N")) end + def test_normalize_module_name_q + buf, dirs, decls = RBS::Parser.parse_signature(<<~EOF) + class Foo + module Bar + module Baz + end + end + end + + module M = Foo::Bar + module N = M::Baz + + module C = D + module D = C + + module E = F + EOF + + env = Environment.new() + env.add_signature(buffer: buf, directives: dirs, decls: decls) + + env = env.resolve_type_names + + assert_equal type_name("::Foo::Bar"), env.normalize_module_name?(type_name("::M")) + assert_equal type_name("::Foo::Bar::Baz"), env.normalize_module_name?(type_name("::N")) + + assert_equal false, env.normalize_module_name?(type_name("::C")) + assert_equal false, env.normalize_module_name?(type_name("::D")) + + assert_nil env.normalize_module_name?(type_name("::E")) + end + + def test_use_resolve buf, dirs, decls = RBS::Parser.parse_signature(<<-RBS) use Object as OB @@ -585,4 +618,32 @@ def test_resolve_type_names_magic_comment__true assert_equal "::s", alias_decl.decl.type.to_s end end + + def test_resolve_type_names_module_alias + buf, dirs, decls = RBS::Parser.parse_signature(<<-RBS) +module M + module N + end + + module N2 = N +end + +class C + include M::N2 +end + RBS + + env = Environment.new + env.add_signature(buffer: buf, directives: dirs, decls: decls) + + env.resolve_type_names.tap do |env| + class_decl = env.class_decls[RBS::TypeName.parse("::C")] + class_decl.decls.each do |d| + d.decl.members[0].tap do |member| + assert_instance_of RBS::AST::Members::Include, member + assert_equal RBS::TypeName.parse("::M::N"), member.name + end + end + end + end end diff --git a/test/rbs/resolver/type_name_resolver_test.rb b/test/rbs/resolver/type_name_resolver_test.rb index 782fa9474..019e78be7 100644 --- a/test/rbs/resolver/type_name_resolver_test.rb +++ b/test/rbs/resolver/type_name_resolver_test.rb @@ -5,147 +5,169 @@ class RBS::Resolver::TypeNameResolverTest < Test::Unit::TestCase include RBS def test_resolve - SignatureManager.new do |manager| - manager.files[Pathname("foo.rbs")] = < [type_name("::MyObject"), nil] + } + ) + + assert_equal( + type_name("::MyObject::name2"), + resolver.resolve(type_name("Alias::name2"), context: nil) + ) + assert_equal( + type_name("::MyObject::name2"), + resolver.resolve(type_name("Alias::name2"), context: [nil, type_name("::MyObject")]) + ) + assert_equal( + type_name("::MyObject::name2"), + resolver.resolve(type_name("name2"), context: [nil, type_name("::MyObject")]) + ) end - def test_object_name - SignatureManager.new do |manager| - manager.files[Pathname("foo.rbs")] = < [type_name("N"), [nil, type_name("::M")]] + } + ) + + assert_equal( + type_name("::M::N"), + resolver.resolve(type_name("M::N2"), context: [nil, RBS::TypeName.parse("::C")]) + ) + + assert_equal( + type_name("::M::N::O"), + resolver.resolve(type_name("M::N2::O"), context: [nil, RBS::TypeName.parse("::C")]) + ) + end -type MyObject::name = ::String -EOF - manager.build do |env| - resolver = Resolver::TypeNameResolver.new(env) - - assert_equal type_name("::MyObject::name"), - resolver.resolve(type_name("name"), context: [nil, RBS::TypeName.parse("::MyObject")]) - assert_equal type_name("::MyObject::name2"), - resolver.resolve(type_name("name2"), context: [nil, RBS::TypeName.parse("::MyObject")]) - end - end + def test_module_alias_cyclic + resolver = Resolver::TypeNameResolver.new( + Set[type_name("::M")], + { + type_name("::M::N1") => [type_name("N2"), [nil, type_name("::M")]], + type_name("::M::N2") => [type_name("N3"), [nil, type_name("::M")]], + type_name("::M::N3") => [type_name("N1"), [nil, type_name("::M")]] + } + ) + + assert_nil resolver.resolve(type_name("M::N1"), context: nil) + assert_nil resolver.resolve(type_name("M::N2"), context: nil) + assert_nil resolver.resolve(type_name("M::N3"), context: nil) end - def test_module_alias - SignatureManager.new do |manager| - manager.files[Pathname("foo.rbs")] = < [type_name("M"), nil], + type_name("::M::N2") => [type_name("M"), nil], + } + ) -class Alias = MyObject + assert_equal type_name("::M::M1"), resolver.resolve(type_name("M::N1::N2::N1::N2::M1"), context: nil) + end + + def test_module_alias_pocke + resolver = Resolver::TypeNameResolver.new( + Set[type_name("::M"), type_name("::M::N"), type_name("::C")], + { + type_name("::M::N2") => [type_name("N"), [nil, type_name("::M")]], + } + ) -type foo = Alias::name2 -EOF - manager.build do |env| - resolver = Resolver::TypeNameResolver.new(env) + assert_equal type_name("::M::N"), resolver.resolve(type_name("M::N2"), context: [nil, type_name("::C")]) + end - assert_equal type_name("::Alias::name2"), - resolver.resolve(type_name("Alias::name2"), context: nil) - end - end + def test_module_alias_absolute + resolver = Resolver::TypeNameResolver.new( + Set[type_name("::M1"), type_name("::M2"), type_name("::Aliases")], + { + type_name("::Aliases::M1") => [type_name("::M1"), [nil, type_name("::Aliases")]], + type_name("::Aliases::M2") => [type_name("::M2"), [nil, type_name("::Aliases")]], + } + ) + + assert_equal type_name("::M2"), resolver.resolve(type_name("Aliases::M2"), context: nil) + assert_equal type_name("::M2"), resolver.resolve(type_name("Aliases::M2"), context: [nil, type_name("::M1")]) end end diff --git a/test/rbs/type_alias_regulartiry_test.rb b/test/rbs/type_alias_regulartiry_test.rb index d78c73cf5..00e56cc35 100644 --- a/test/rbs/type_alias_regulartiry_test.rb +++ b/test/rbs/type_alias_regulartiry_test.rb @@ -82,7 +82,7 @@ module Bar = Foo assert_operator validator, :nonregular?, RBS::TypeName.parse("::Bar::baz") assert_equal( - parse_type("::Bar::baz[::Foo::bar[T]]", variables: [:T]), + parse_type("::Foo::baz[::Foo::bar[T]]", variables: [:T]), validator.nonregular?(RBS::TypeName.parse("::Foo::baz")).nonregular_type ) end diff --git a/test/validator_test.rb b/test/validator_test.rb index 8772a8695..edf2c6492 100644 --- a/test/validator_test.rb +++ b/test/validator_test.rb @@ -27,7 +27,7 @@ class Foo manager.build do |env| root = nil - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) validator.validate_type(parse_type("::Foo"), context: root) @@ -83,7 +83,7 @@ def test_validate_recursive_type_alias EOF manager.build do |env| - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) env.type_alias_decls.each do |name, decl| assert_raises RBS::RecursiveTypeAliasError do @@ -113,7 +113,7 @@ class Bar type record = { foo: record } EOF manager.build do |env| - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) env.type_alias_decls.each do |name, entry| @@ -134,7 +134,7 @@ def test_generic_type_aliases EOF manager.build do |env| - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) validator.validate_type_alias(entry: env.type_alias_decls[type_name("::foo")]) @@ -166,7 +166,7 @@ def test_generic_type_bound EOF manager.build do |env| - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) validator.validate_type_alias(entry: env.type_alias_decls[type_name("::foo")]) @@ -197,7 +197,7 @@ class Foo[in A, out B] RBS manager.build do |env| - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) validator.validate_type_alias(entry: env.type_alias_decls[type_name("::foo")]) @@ -212,7 +212,7 @@ def test_type_alias_unknown_type RBS manager.build do |env| - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) # No error is raised. @@ -221,7 +221,7 @@ def test_type_alias_unknown_type # Passing a block and validating the given type raises an error. assert_raises RBS::NoTypeFoundError do validator.validate_type_alias(entry: env.type_alias_decls[type_name("::foo")]) do |type| - validator.validate_type(type, context: []) + validator.validate_type(type, context: nil) end end end @@ -239,7 +239,7 @@ class Baz = Baz EOF manager.build do |env| - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) env.class_alias_decls[RBS::TypeName.parse("::Foo")].tap do |entry| @@ -293,7 +293,7 @@ class Foo::Baz = Integer manager.build do |env| root = nil - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) validator.validate_type(parse_type("Bar::Baz"), context: root) @@ -315,7 +315,7 @@ class Foo::Baz = Integer manager.build do |env| root = nil - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) validator.validate_type(parse_type("singleton(Bar::Baz)"), context: root) @@ -336,7 +336,7 @@ module Bar = Foo manager.build do |env| root = nil - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) validator.validate_type(parse_type("Bar::list[Bar]"), context: root) @@ -358,7 +358,7 @@ class Baz = Numeric EOF manager.build do |env| - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) validator.validate_type_alias(entry: env.type_alias_decls[RBS::TypeName.parse("::Foo::list")]) @@ -380,7 +380,7 @@ def foo: () -> A manager.build do |env| root = nil - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) validator.validate_type(parse_type("::A"), context: root) @@ -408,7 +408,7 @@ class Foo EOF manager.build do |env| - resolver = RBS::Resolver::TypeNameResolver.new(env) + resolver = RBS::Resolver::TypeNameResolver.build(env) validator = RBS::Validator.new(env: env, resolver: resolver) env.class_decls[RBS::TypeName.parse("::Foo")].decls.first.decl.members.tap do |members|