From fb39d5898f8b4ea0d7a4cea596d91cdf0dc81d8b Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 17 Feb 2026 10:27:06 -0500 Subject: [PATCH 1/8] [ruby/prism] rbs-inline https://github.com/ruby/prism/commit/1fdd4c5a49 --- lib/prism.rb | 33 ++ lib/prism/desugar_compiler.rb | 78 ++++- lib/prism/lex_compat.rb | 143 +++++--- lib/prism/node_ext.rb | 165 +++++++++- lib/prism/parse_result.rb | 311 ++++++++++++++++-- lib/prism/parse_result/comments.rb | 42 ++- lib/prism/parse_result/errors.rb | 7 +- lib/prism/parse_result/newlines.rb | 58 +++- lib/prism/pattern.rb | 50 ++- lib/prism/prism.gemspec | 40 ++- lib/prism/relocation.rb | 185 ++++++++++- lib/prism/string_query.rb | 16 +- lib/prism/translation.rb | 1 + lib/prism/translation/parser_current.rb | 1 - prism/templates/lib/prism/compiler.rb.erb | 9 + prism/templates/lib/prism/dispatcher.rb.erb | 23 +- prism/templates/lib/prism/dot_visitor.rb.erb | 36 +- prism/templates/lib/prism/dsl.rb.erb | 24 ++ .../lib/prism/inspect_visitor.rb.erb | 23 +- .../lib/prism/mutation_compiler.rb.erb | 3 + prism/templates/lib/prism/node.rb.erb | 161 ++++++++- prism/templates/lib/prism/reflection.rb.erb | 12 +- prism/templates/lib/prism/serialize.rb.erb | 244 ++++++++++---- prism/templates/lib/prism/visitor.rb.erb | 17 + prism/templates/template.rb | 31 +- 25 files changed, 1450 insertions(+), 263 deletions(-) diff --git a/lib/prism.rb b/lib/prism.rb index 5b3ce8752c267d..c7530e5874045d 100644 --- a/lib/prism.rb +++ b/lib/prism.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rbs_inline: enabled # :markup: markdown # The Prism Ruby parser. @@ -37,6 +38,8 @@ module Prism # Raised when requested to parse as the currently running Ruby version but Prism has no support for it. class CurrentVersionError < ArgumentError # Initialize a new exception for the given ruby version string. + #-- + #: (String version) -> void def initialize(version) message = +"invalid version: Requested to parse as `version: 'current'`; " segments = @@ -61,6 +64,8 @@ def initialize(version) # resembles the return value of Ripper.lex. # # For supported options, see Prism.parse. + #-- + #: (String source, **untyped options) -> LexCompat::Result def self.lex_compat(source, **options) LexCompat.new(source, **options).result # steep:ignore end @@ -69,9 +74,37 @@ def self.lex_compat(source, **options) # load(source, serialized, freeze) -> ParseResult # # Load the serialized AST using the source as a reference into a tree. + #-- + #: (String source, String serialized, ?bool freeze) -> ParseResult def self.load(source, serialized, freeze = false) Serialize.load_parse(source, serialized, freeze) end + + # @rbs! + # VERSION: String + # BACKEND: :CEXT | :FFI + # + # interface _Stream + # def gets: (?Integer integer) -> (String | nil) + # end + # + # def self.parse: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult + # def self.profile: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> void + # def self.lex: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> LexResult + # def self.parse_lex: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseLexResult + # def self.dump: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> String + # def self.parse_comments: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> Array[Comment] + # def self.parse_success?: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool + # def self.parse_failure?: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool + # def self.parse_stream: (_Stream stream, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult + # def self.parse_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult + # def self.profile_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> void + # def self.lex_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> LexResult + # def self.parse_lex_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseLexResult + # def self.dump_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> String + # def self.parse_file_comments: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> Array[Comment] + # def self.parse_file_success?: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool + # def self.parse_file_failure?: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool end require_relative "prism/polyfill/byteindex" diff --git a/lib/prism/desugar_compiler.rb b/lib/prism/desugar_compiler.rb index 7d4201c1c47b50..e08faa321919b0 100644 --- a/lib/prism/desugar_compiler.rb +++ b/lib/prism/desugar_compiler.rb @@ -1,12 +1,17 @@ # frozen_string_literal: true +# rbs_inline: enabled # :markup: markdown module Prism class DesugarAndWriteNode # :nodoc: include DSL - attr_reader :node, :default_source, :read_class, :write_class, :arguments + attr_reader :node #: ClassVariableAndWriteNode | ConstantAndWriteNode | GlobalVariableAndWriteNode | InstanceVariableAndWriteNode | LocalVariableAndWriteNode + attr_reader :default_source #: Source + attr_reader :read_class, :write_class #: Symbol + attr_reader :arguments #: Hash[Symbol, untyped] + #: ((ClassVariableAndWriteNode | ConstantAndWriteNode | GlobalVariableAndWriteNode | InstanceVariableAndWriteNode | LocalVariableAndWriteNode) node, Source default_source, Symbol read_class, Symbol write_class, **untyped arguments) -> void def initialize(node, default_source, read_class, write_class, **arguments) @node = node @default_source = default_source @@ -16,6 +21,8 @@ def initialize(node, default_source, read_class, write_class, **arguments) end # Desugar `x &&= y` to `x && x = y` + #-- + #: () -> node def compile and_node( location: node.location, @@ -36,8 +43,12 @@ def compile class DesugarOrWriteDefinedNode # :nodoc: include DSL - attr_reader :node, :default_source, :read_class, :write_class, :arguments + attr_reader :node #: ClassVariableOrWriteNode | ConstantOrWriteNode | GlobalVariableOrWriteNode + attr_reader :default_source #: Source + attr_reader :read_class, :write_class #: Symbol + attr_reader :arguments #: Hash[Symbol, untyped] + #: ((ClassVariableOrWriteNode | ConstantOrWriteNode | GlobalVariableOrWriteNode) node, Source default_source, Symbol read_class, Symbol write_class, **untyped arguments) -> void def initialize(node, default_source, read_class, write_class, **arguments) @node = node @default_source = default_source @@ -47,6 +58,8 @@ def initialize(node, default_source, read_class, write_class, **arguments) end # Desugar `x ||= y` to `defined?(x) ? x : x = y` + #-- + #: () -> node def compile if_node( location: node.location, @@ -87,8 +100,12 @@ def compile class DesugarOperatorWriteNode # :nodoc: include DSL - attr_reader :node, :default_source, :read_class, :write_class, :arguments + attr_reader :node #: ClassVariableOperatorWriteNode | ConstantOperatorWriteNode | GlobalVariableOperatorWriteNode | InstanceVariableOperatorWriteNode | LocalVariableOperatorWriteNode + attr_reader :default_source #: Source + attr_reader :read_class, :write_class #: Symbol + attr_reader :arguments #: Hash[Symbol, untyped] + #: ((ClassVariableOperatorWriteNode | ConstantOperatorWriteNode | GlobalVariableOperatorWriteNode | InstanceVariableOperatorWriteNode | LocalVariableOperatorWriteNode) node, Source default_source, Symbol read_class, Symbol write_class, **untyped arguments) -> void def initialize(node, default_source, read_class, write_class, **arguments) @node = node @default_source = default_source @@ -98,6 +115,8 @@ def initialize(node, default_source, read_class, write_class, **arguments) end # Desugar `x += y` to `x = x + y` + #-- + #: () -> node def compile binary_operator_loc = node.binary_operator_loc.chop @@ -131,8 +150,12 @@ def compile class DesugarOrWriteNode # :nodoc: include DSL - attr_reader :node, :default_source, :read_class, :write_class, :arguments + attr_reader :node #: InstanceVariableOrWriteNode | LocalVariableOrWriteNode + attr_reader :default_source #: Source + attr_reader :read_class, :write_class #: Symbol + attr_reader :arguments #: Hash[Symbol, untyped] + #: ((InstanceVariableOrWriteNode | LocalVariableOrWriteNode) node, Source default_source, Symbol read_class, Symbol write_class, **untyped arguments) -> void def initialize(node, default_source, read_class, write_class, **arguments) @node = node @default_source = default_source @@ -142,6 +165,8 @@ def initialize(node, default_source, read_class, write_class, **arguments) end # Desugar `x ||= y` to `x || x = y` + #-- + #: () -> node def compile or_node( location: node.location, @@ -162,90 +187,105 @@ def compile private_constant :DesugarAndWriteNode, :DesugarOrWriteNode, :DesugarOrWriteDefinedNode, :DesugarOperatorWriteNode class ClassVariableAndWriteNode + #: () -> node def desugar # :nodoc: DesugarAndWriteNode.new(self, source, :class_variable_read_node, :class_variable_write_node, name: name).compile end end class ClassVariableOrWriteNode + #: () -> node def desugar # :nodoc: DesugarOrWriteDefinedNode.new(self, source, :class_variable_read_node, :class_variable_write_node, name: name).compile end end class ClassVariableOperatorWriteNode + #: () -> node def desugar # :nodoc: DesugarOperatorWriteNode.new(self, source, :class_variable_read_node, :class_variable_write_node, name: name).compile end end class ConstantAndWriteNode + #: () -> node def desugar # :nodoc: DesugarAndWriteNode.new(self, source, :constant_read_node, :constant_write_node, name: name).compile end end class ConstantOrWriteNode + #: () -> node def desugar # :nodoc: DesugarOrWriteDefinedNode.new(self, source, :constant_read_node, :constant_write_node, name: name).compile end end class ConstantOperatorWriteNode + #: () -> node def desugar # :nodoc: DesugarOperatorWriteNode.new(self, source, :constant_read_node, :constant_write_node, name: name).compile end end class GlobalVariableAndWriteNode + #: () -> node def desugar # :nodoc: DesugarAndWriteNode.new(self, source, :global_variable_read_node, :global_variable_write_node, name: name).compile end end class GlobalVariableOrWriteNode + #: () -> node def desugar # :nodoc: DesugarOrWriteDefinedNode.new(self, source, :global_variable_read_node, :global_variable_write_node, name: name).compile end end class GlobalVariableOperatorWriteNode + #: () -> node def desugar # :nodoc: DesugarOperatorWriteNode.new(self, source, :global_variable_read_node, :global_variable_write_node, name: name).compile end end class InstanceVariableAndWriteNode + #: () -> node def desugar # :nodoc: DesugarAndWriteNode.new(self, source, :instance_variable_read_node, :instance_variable_write_node, name: name).compile end end class InstanceVariableOrWriteNode + #: () -> node def desugar # :nodoc: DesugarOrWriteNode.new(self, source, :instance_variable_read_node, :instance_variable_write_node, name: name).compile end end class InstanceVariableOperatorWriteNode + #: () -> node def desugar # :nodoc: DesugarOperatorWriteNode.new(self, source, :instance_variable_read_node, :instance_variable_write_node, name: name).compile end end class LocalVariableAndWriteNode + #: () -> node def desugar # :nodoc: DesugarAndWriteNode.new(self, source, :local_variable_read_node, :local_variable_write_node, name: name, depth: depth).compile end end class LocalVariableOrWriteNode + #: () -> node def desugar # :nodoc: DesugarOrWriteNode.new(self, source, :local_variable_read_node, :local_variable_write_node, name: name, depth: depth).compile end end class LocalVariableOperatorWriteNode + #: () -> node def desugar # :nodoc: DesugarOperatorWriteNode.new(self, source, :local_variable_read_node, :local_variable_write_node, name: name, depth: depth).compile end @@ -259,6 +299,8 @@ class DesugarCompiler < MutationCompiler # becomes # # `@@foo && @@foo = bar` + #-- + #: (ClassVariableAndWriteNode node) -> node def visit_class_variable_and_write_node(node) node.desugar end @@ -268,6 +310,8 @@ def visit_class_variable_and_write_node(node) # becomes # # `defined?(@@foo) ? @@foo : @@foo = bar` + #-- + #: (ClassVariableOrWriteNode node) -> node def visit_class_variable_or_write_node(node) node.desugar end @@ -277,6 +321,8 @@ def visit_class_variable_or_write_node(node) # becomes # # `@@foo = @@foo + bar` + #-- + #: (ClassVariableOperatorWriteNode node) -> node def visit_class_variable_operator_write_node(node) node.desugar end @@ -286,6 +332,8 @@ def visit_class_variable_operator_write_node(node) # becomes # # `Foo && Foo = bar` + #-- + #: (ConstantAndWriteNode node) -> node def visit_constant_and_write_node(node) node.desugar end @@ -295,6 +343,8 @@ def visit_constant_and_write_node(node) # becomes # # `defined?(Foo) ? Foo : Foo = bar` + #-- + #: (ConstantOrWriteNode node) -> node def visit_constant_or_write_node(node) node.desugar end @@ -304,6 +354,8 @@ def visit_constant_or_write_node(node) # becomes # # `Foo = Foo + bar` + #-- + #: (ConstantOperatorWriteNode node) -> node def visit_constant_operator_write_node(node) node.desugar end @@ -313,6 +365,8 @@ def visit_constant_operator_write_node(node) # becomes # # `$foo && $foo = bar` + #-- + #: (GlobalVariableAndWriteNode node) -> node def visit_global_variable_and_write_node(node) node.desugar end @@ -322,6 +376,8 @@ def visit_global_variable_and_write_node(node) # becomes # # `defined?($foo) ? $foo : $foo = bar` + #-- + #: (GlobalVariableOrWriteNode node) -> node def visit_global_variable_or_write_node(node) node.desugar end @@ -331,6 +387,8 @@ def visit_global_variable_or_write_node(node) # becomes # # `$foo = $foo + bar` + #-- + #: (GlobalVariableOperatorWriteNode node) -> node def visit_global_variable_operator_write_node(node) node.desugar end @@ -340,6 +398,8 @@ def visit_global_variable_operator_write_node(node) # becomes # # `@foo && @foo = bar` + #-- + #: (InstanceVariableAndWriteNode node) -> node def visit_instance_variable_and_write_node(node) node.desugar end @@ -349,6 +409,8 @@ def visit_instance_variable_and_write_node(node) # becomes # # `@foo || @foo = bar` + #-- + #: (InstanceVariableOrWriteNode node) -> node def visit_instance_variable_or_write_node(node) node.desugar end @@ -358,6 +420,8 @@ def visit_instance_variable_or_write_node(node) # becomes # # `@foo = @foo + bar` + #-- + #: (InstanceVariableOperatorWriteNode node) -> node def visit_instance_variable_operator_write_node(node) node.desugar end @@ -367,6 +431,8 @@ def visit_instance_variable_operator_write_node(node) # becomes # # `foo && foo = bar` + #-- + #: (LocalVariableAndWriteNode node) -> node def visit_local_variable_and_write_node(node) node.desugar end @@ -376,6 +442,8 @@ def visit_local_variable_and_write_node(node) # becomes # # `foo || foo = bar` + #-- + #: (LocalVariableOrWriteNode node) -> node def visit_local_variable_or_write_node(node) node.desugar end @@ -385,6 +453,8 @@ def visit_local_variable_or_write_node(node) # becomes # # `foo = foo + bar` + #-- + #: (LocalVariableOperatorWriteNode node) -> node def visit_local_variable_operator_write_node(node) node.desugar end diff --git a/lib/prism/lex_compat.rb b/lib/prism/lex_compat.rb index 3d5cbfcddc0edd..bdef99acfc9535 100644 --- a/lib/prism/lex_compat.rb +++ b/lib/prism/lex_compat.rb @@ -1,25 +1,47 @@ # frozen_string_literal: true +# rbs_inline: enabled # :markup: markdown module Prism + # @rbs! + # module Translation + # class Ripper + # EXPR_BEG: Integer + # + # class Lexer < Ripper + # class State + # def self.[]: (Integer value) -> State + # end + # end + # end + # end + # This class is responsible for lexing the source using prism and then # converting those tokens to be compatible with Ripper. In the vast majority # of cases, this is a one-to-one mapping of the token type. Everything else # generally lines up. However, there are a few cases that require special # handling. class LexCompat # :nodoc: + # @rbs! + # # A token produced by the Ripper lexer that Prism is replicating. + # type lex_compat_token = [[Integer, Integer], Symbol, String, untyped] + # A result class specialized for holding tokens produced by the lexer. class Result < Prism::Result # The list of tokens that were produced by the lexer. - attr_reader :value + attr_reader :value #: Array[lex_compat_token] # Create a new lex compat result object with the given values. + #-- + #: (Array[lex_compat_token] value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, Source source) -> void def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) @value = value super(comments, magic_comments, data_loc, errors, warnings, source) end # Implement the hash pattern matching interface for Result. + #-- + #: (Array[Symbol]? keys) -> Hash[Symbol, untyped] def deconstruct_keys(keys) # :nodoc: super.merge!(value: value) end @@ -205,16 +227,19 @@ module Heredoc # :nodoc: # order back into the token stream and set the state of the last token to # the state that the heredoc was opened in. class PlainHeredoc # :nodoc: - attr_reader :tokens + attr_reader :tokens #: Array[lex_compat_token] + #: () -> void def initialize @tokens = [] end + #: (lex_compat_token token) -> void def <<(token) tokens << token end + #: () -> Array[lex_compat_token] def to_a tokens end @@ -224,21 +249,25 @@ def to_a # that need to be split on "\\\n" to mimic Ripper's behavior. We also need # to keep track of the state that the heredoc was opened in. class DashHeredoc # :nodoc: - attr_reader :split, :tokens + attr_reader :split #: bool + attr_reader :tokens #: Array[lex_compat_token] + #: (bool split) -> void def initialize(split) @split = split @tokens = [] end + #: (lex_compat_token token) -> void def <<(token) tokens << token end + #: () -> Array[lex_compat_token] def to_a embexpr_balance = 0 - tokens.each_with_object([]) do |token, results| #$ Array[Token] + tokens.each_with_object([]) do |token, results| #$ Array[lex_compat_token] case token[1] when :on_embexpr_beg embexpr_balance += 1 @@ -285,8 +314,13 @@ def to_a class DedentingHeredoc # :nodoc: TAB_WIDTH = 8 - attr_reader :tokens, :dedent_next, :dedent, :embexpr_balance + attr_reader :tokens #: Array[lex_compat_token] + attr_reader :dedent_next #: bool + attr_reader :dedent #: Integer? + attr_reader :embexpr_balance #: Integer + # @rbs @ended_on_newline: bool + #: () -> void def initialize @tokens = [] @dedent_next = true @@ -298,6 +332,8 @@ def initialize # As tokens are coming in, we track the minimum amount of common leading # whitespace on plain string content tokens. This allows us to later # remove that amount of whitespace from the beginning of each line. + # + #: (lex_compat_token token) -> void def <<(token) case token[1] when :on_embexpr_beg, :on_heredoc_beg @@ -310,7 +346,7 @@ def <<(token) line = token[2] if dedent_next && !(line.strip.empty? && line.end_with?("\n")) - leading = line[/\A(\s*)\n?/, 1] + leading = line[/\A(\s*)\n?/, 1] #: String next_dedent = 0 leading.each_char do |char| @@ -335,11 +371,12 @@ def <<(token) tokens << token end + #: () -> Array[lex_compat_token] def to_a # If every line in the heredoc is blank, we still need to split up the # string content token into multiple tokens. if dedent.nil? - results = [] #: Array[Token] + results = [] #: Array[lex_compat_token] embexpr_balance = 0 tokens.each do |token| @@ -374,7 +411,7 @@ def to_a # If the minimum common whitespace is 0, then we need to concatenate # string nodes together that are immediately adjacent. if dedent == 0 - results = [] #: Array[Token] + results = [] #: Array[lex_compat_token] embexpr_balance = 0 index = 0 @@ -407,7 +444,7 @@ def to_a # insert on_ignored_sp tokens for the amount of dedent that we need to # perform. We also need to remove the dedent from the beginning of # each line of plain string content tokens. - results = [] #: Array[Token] + results = [] #: Array[lex_compat_token] dedent_next = true embexpr_balance = 0 @@ -446,7 +483,8 @@ def to_a # line or this line doesn't start with whitespace, then we # should concatenate the rest of the string to match ripper. if dedent == 0 && (!dedent_next || !line.start_with?(/\s/)) - line = splits[index..].join + unjoined = splits[index..] #: Array[String] + line = unjoined.join index = splits.length end @@ -511,6 +549,8 @@ def to_a # Here we will split between the two types of heredocs and return the # object that will store their tokens. + #-- + #: (lex_compat_token opening) -> (PlainHeredoc | DashHeredoc | DedentingHeredoc) def self.build(opening) case opening[2][2] when "~" @@ -530,31 +570,38 @@ def self.build(opening) BOM_FLUSHED = RUBY_VERSION >= "3.3.0" private_constant :BOM_FLUSHED - attr_reader :options + attr_reader :options #: Hash[Symbol, untyped] + # @rbs @source: String - def initialize(code, **options) - @code = code + #: (String source, **untyped options) -> void + def initialize(source, **options) + @source = source @options = options end + #: () -> Result def result - tokens = [] #: Array[LexCompat::Token] + tokens = [] #: Array[lex_compat_token] state = :default heredoc_stack = [[]] #: Array[Array[Heredoc::PlainHeredoc | Heredoc::DashHeredoc | Heredoc::DedentingHeredoc]] - result = Prism.lex(@code, **options) + result = Prism.lex(@source, **options) source = result.source result_value = result.value - previous_state = nil #: State? + previous_state = nil #: Translation::Ripper::Lexer::State? last_heredoc_end = nil #: Integer? - eof_token = nil + eof_token = nil #: Token? bom = source.slice(0, 3) == "\xEF\xBB\xBF" - result_value.each_with_index do |(token, lex_state), index| - lineno = token.location.start_line - column = token.location.start_column + result_value.each_with_index do |(prism_token, prism_state), index| + lineno = prism_token.location.start_line + column = prism_token.location.start_column + + event = RIPPER.fetch(prism_token.type) + value = prism_token.value + lex_state = Translation::Ripper::Lexer::State[prism_state] # If there's a UTF-8 byte-order mark as the start of the file, then for # certain tokens ripper sets the first token back by 3 bytes. It also @@ -566,43 +613,38 @@ def result if index == 0 && column == 0 && !BOM_FLUSHED flushed = - case token.type + case prism_token.type when :BACK_REFERENCE, :INSTANCE_VARIABLE, :CLASS_VARIABLE, :GLOBAL_VARIABLE, :NUMBERED_REFERENCE, :PERCENT_LOWER_I, :PERCENT_LOWER_X, :PERCENT_LOWER_W, :PERCENT_UPPER_I, :PERCENT_UPPER_W, :STRING_BEGIN true when :REGEXP_BEGIN, :SYMBOL_BEGIN - token.value.start_with?("%") + value.start_with?("%") else false end unless flushed column -= 3 - value = token.value value.prepend(String.new("\xEF\xBB\xBF", encoding: value.encoding)) end end end - event = RIPPER.fetch(token.type) - value = token.value - lex_state = Translation::Ripper::Lexer::State[lex_state] - - token = + lex_compat_token = case event when :on___end__ # Ripper doesn't include the rest of the token in the event, so we need to # trim it down to just the content on the first line. - value = value[0..value.index("\n")] + value = value[0..value.index("\n")] #: String [[lineno, column], event, value, lex_state] when :on_comment [[lineno, column], event, value, lex_state] when :on_heredoc_end # Heredoc end tokens can be emitted in an odd order, so we don't # want to bother comparing the state on them. - last_heredoc_end = token.location.end_offset + last_heredoc_end = prism_token.location.end_offset [[lineno, column], event, value, lex_state] when :on_embexpr_end [[lineno, column], event, value, lex_state] @@ -615,7 +657,7 @@ def result end tokens << [[lineno, column], event, line, lex_state] end - tokens.pop + tokens.pop #: lex_compat_token when :on_regexp_end # On regex end, Ripper scans and then sets end state, so the ripper # lexed output is begin, when it should be end. prism sets lex state @@ -647,7 +689,7 @@ def result [[lineno, column], event, value, lex_state] when :on_eof - eof_token = token + eof_token = prism_token previous_token = result_value[index - 1][0] # If we're at the end of the file and the previous token was a @@ -662,7 +704,7 @@ def result # Use the greater offset of the two to determine the start of # the trailing whitespace. start_offset = [previous_token.location.end_offset, last_heredoc_end].compact.max - end_offset = token.location.start_offset + end_offset = prism_token.location.start_offset if start_offset < end_offset if bom @@ -677,7 +719,7 @@ def result [[lineno, column], event, value, lex_state] else [[lineno, column], event, value, lex_state] - end + end #: lex_compat_token previous_state = lex_state @@ -694,19 +736,19 @@ def result when :default # The default state is when there are no heredocs at all. In this # state we can append the token to the list of tokens and move on. - tokens << token + tokens << lex_compat_token # If we get the declaration of a heredoc, then we open a new heredoc # and move into the heredoc_opened state. if event == :on_heredoc_beg state = :heredoc_opened - heredoc_stack.last << Heredoc.build(token) + heredoc_stack.last << Heredoc.build(lex_compat_token) end when :heredoc_opened # The heredoc_opened state is when we've seen the declaration of a # heredoc and are now lexing the body of the heredoc. In this state we # push tokens onto the most recently created heredoc. - heredoc_stack.last.last << token + heredoc_stack.last.last << lex_compat_token case event when :on_heredoc_beg @@ -714,7 +756,7 @@ def result # heredoc, this means we have nested heredocs. In this case we'll # push a new heredoc onto the stack and stay in the heredoc_opened # state since we're now lexing the body of the new heredoc. - heredoc_stack << [Heredoc.build(token)] + heredoc_stack << [Heredoc.build(lex_compat_token)] when :on_heredoc_end # If we receive the end of a heredoc, then we're done lexing the # body of the heredoc. In this case we now have a completed heredoc @@ -723,10 +765,10 @@ def result state = :heredoc_closed end when :heredoc_closed - if %i[on_nl on_ignored_nl on_comment].include?(event) || (event == :on_tstring_content && value.end_with?("\n")) + if %i[on_nl on_ignored_nl on_comment].include?(event) || ((event == :on_tstring_content) && value.end_with?("\n")) if heredoc_stack.size > 1 - flushing = heredoc_stack.pop - heredoc_stack.last.last << token + flushing = heredoc_stack.pop #: Array[Heredoc::PlainHeredoc | Heredoc::DashHeredoc | Heredoc::DedentingHeredoc] + heredoc_stack.last.last << lex_compat_token flushing.each do |heredoc| heredoc.to_a.each do |flushed_token| @@ -738,12 +780,12 @@ def result next end elsif event == :on_heredoc_beg - tokens << token + tokens << lex_compat_token state = :heredoc_opened - heredoc_stack.last << Heredoc.build(token) + heredoc_stack.last << Heredoc.build(lex_compat_token) next elsif heredoc_stack.size > 1 - heredoc_stack[-2].last << token + heredoc_stack[-2].last << lex_compat_token next end @@ -754,13 +796,15 @@ def result heredoc_stack.last.clear state = :default - tokens << token + tokens << lex_compat_token end end # Drop the EOF token from the list. The EOF token may not be # present if the source was syntax invalid - tokens = tokens[0...-1] if tokens.dig(-1, 1) == :on_eof + if tokens.dig(-1, 1) == :on_eof + tokens = tokens[0...-1] #: Array[lex_compat_token] + end # We sort by location because Ripper.lex sorts. tokens.sort_by! do |token| @@ -775,8 +819,9 @@ def result private + #: (Array[lex_compat_token] tokens, Source source, Location? data_loc, bool bom, Token? eof_token) -> Array[lex_compat_token] def post_process_tokens(tokens, source, data_loc, bom, eof_token) - new_tokens = [] + new_tokens = [] #: Array[lex_compat_token] prev_token_state = Translation::Ripper::Lexer::State[Translation::Ripper::EXPR_BEG] prev_token_end = bom ? 3 : 0 @@ -806,8 +851,8 @@ def post_process_tokens(tokens, source, data_loc, bom, eof_token) next_whitespace_index = continuation_index + 1 next_whitespace_index += 1 if sp_value.byteslice(next_whitespace_index) == "\r" next_whitespace_index += 1 - first_whitespace = sp_value[0...continuation_index] - continuation = sp_value[continuation_index...next_whitespace_index] + first_whitespace = sp_value[0...continuation_index] #: String + continuation = sp_value[continuation_index...next_whitespace_index] #: String second_whitespace = sp_value[next_whitespace_index..] || "" new_tokens << [[sp_line, sp_column], :on_sp, first_whitespace, prev_token_state] unless first_whitespace.empty? diff --git a/lib/prism/node_ext.rb b/lib/prism/node_ext.rb index a05123d1bb3aa5..57593a16125cd7 100644 --- a/lib/prism/node_ext.rb +++ b/lib/prism/node_ext.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rbs_inline: enabled # :markup: markdown #-- @@ -7,6 +8,7 @@ #++ module Prism class Node + #: (*String replacements) -> void def deprecated(*replacements) # :nodoc: location = caller_locations(1, 1) location = location[0].label if location @@ -23,7 +25,9 @@ def deprecated(*replacements) # :nodoc: module RegularExpressionOptions # :nodoc: # Returns a numeric value that represents the flags that were used to create # the regular expression. - def options + #-- + #: (Integer flags) -> Integer + def self.options(flags) o = 0 o |= Regexp::IGNORECASE if flags.anybits?(RegularExpressionFlags::IGNORE_CASE) o |= Regexp::EXTENDED if flags.anybits?(RegularExpressionFlags::EXTENDED) @@ -35,43 +39,87 @@ def options end class InterpolatedMatchLastLineNode < Node - include RegularExpressionOptions + # Returns a numeric value that represents the flags that were used to create + # the regular expression. + #-- + #: () -> Integer + def options + RegularExpressionOptions.options(flags) + end end class InterpolatedRegularExpressionNode < Node - include RegularExpressionOptions + # Returns a numeric value that represents the flags that were used to create + # the regular expression. + #-- + #: () -> Integer + def options + RegularExpressionOptions.options(flags) + end end class MatchLastLineNode < Node - include RegularExpressionOptions + # Returns a numeric value that represents the flags that were used to create + # the regular expression. + #-- + #: () -> Integer + def options + RegularExpressionOptions.options(flags) + end end class RegularExpressionNode < Node - include RegularExpressionOptions + # Returns a numeric value that represents the flags that were used to create + # the regular expression. + #-- + #: () -> Integer + def options + RegularExpressionOptions.options(flags) + end end private_constant :RegularExpressionOptions module HeredocQuery # :nodoc: # Returns true if this node was represented as a heredoc in the source code. - def heredoc? + #-- + #: (String? opening) -> bool? + def self.heredoc?(opening) + # @type self: InterpolatedStringNode | InterpolatedXStringNode | StringNode | XStringNode opening&.start_with?("<<") end end class InterpolatedStringNode < Node - include HeredocQuery + # Returns true if this node was represented as a heredoc in the source code. + #-- + #: () -> bool? + def heredoc? + HeredocQuery.heredoc?(opening) + end end class InterpolatedXStringNode < Node - include HeredocQuery + # Returns true if this node was represented as a heredoc in the source code. + #-- + #: () -> bool? + def heredoc? + HeredocQuery.heredoc?(opening) + end end class StringNode < Node - include HeredocQuery + # Returns true if this node was represented as a heredoc in the source code. + #-- + #: () -> bool? + def heredoc? + HeredocQuery.heredoc?(opening) + end # Occasionally it's helpful to treat a string as if it were interpolated so # that there's a consistent interface for working with strings. + #-- + #: () -> InterpolatedStringNode def to_interpolated InterpolatedStringNode.new( source, @@ -86,10 +134,17 @@ def to_interpolated end class XStringNode < Node - include HeredocQuery + # Returns true if this node was represented as a heredoc in the source code. + #-- + #: () -> bool? + def heredoc? + HeredocQuery.heredoc?(opening) + end # Occasionally it's helpful to treat a string as if it were interpolated so # that there's a consistent interface for working with strings. + #-- + #: () -> InterpolatedXStringNode def to_interpolated InterpolatedXStringNode.new( source, @@ -107,6 +162,8 @@ def to_interpolated class ImaginaryNode < Node # Returns the value of the node as a Ruby Complex. + #-- + #: () -> Complex def value Complex(0, numeric.value) end @@ -114,12 +171,16 @@ def value class RationalNode < Node # Returns the value of the node as a Ruby Rational. + #-- + #: () -> Rational def value Rational(numerator, denominator) end # Returns the value of the node as an IntegerNode or a FloatNode. This # method is deprecated in favor of #value or #numerator/#denominator. + #-- + #: () -> (IntegerNode | FloatNode) def numeric deprecated("value", "numerator", "denominator") @@ -134,11 +195,15 @@ def numeric class ConstantReadNode < Node # Returns the list of parts for the full name of this constant. # For example: [:Foo] + #-- + #: () -> Array[Symbol] def full_name_parts [name] end # Returns the full name of this constant. For example: "Foo" + #-- + #: () -> String def full_name name.to_s end @@ -147,11 +212,15 @@ def full_name class ConstantWriteNode < Node # Returns the list of parts for the full name of this constant. # For example: [:Foo] + #-- + #: () -> Array[Symbol] def full_name_parts [name] end # Returns the full name of this constant. For example: "Foo" + #-- + #: () -> String def full_name name.to_s end @@ -173,6 +242,8 @@ class MissingNodesInConstantPathError < StandardError; end # Returns the list of parts for the full name of this constant path. # For example: [:Foo, :Bar] + #-- + #: () -> Array[Symbol] def full_name_parts parts = [] #: Array[Symbol] current = self #: node? @@ -195,6 +266,8 @@ def full_name_parts end # Returns the full name of this constant path. For example: "Foo::Bar" + #-- + #: () -> String def full_name full_name_parts.join("::") end @@ -202,10 +275,12 @@ def full_name # Previously, we had a child node on this class that contained either a # constant read or a missing node. To not cause a breaking change, we # continue to supply that API. + #-- + #: () -> (ConstantReadNode | MissingNode) def child deprecated("name", "name_loc") - if name + if (name = self.name) ConstantReadNode.new(source, -1, name_loc, 0, name) else MissingNode.new(source, -1, location, 0) @@ -216,9 +291,11 @@ def child class ConstantPathTargetNode < Node # Returns the list of parts for the full name of this constant path. # For example: [:Foo, :Bar] + #-- + #: () -> Array[Symbol] def full_name_parts parts = - case parent + case (parent = self.parent) when ConstantPathNode, ConstantReadNode parent.full_name_parts when nil @@ -228,7 +305,7 @@ def full_name_parts raise ConstantPathNode::DynamicPartsInConstantPathError, "Constant target path contains dynamic parts. Cannot compute full name" end - if name.nil? + if (name = self.name).nil? raise ConstantPathNode::MissingNodesInConstantPathError, "Constant target path contains missing nodes. Cannot compute full name" end @@ -236,6 +313,8 @@ def full_name_parts end # Returns the full name of this constant path. For example: "Foo::Bar" + #-- + #: () -> String def full_name full_name_parts.join("::") end @@ -243,10 +322,12 @@ def full_name # Previously, we had a child node on this class that contained either a # constant read or a missing node. To not cause a breaking change, we # continue to supply that API. + #-- + #: () -> (ConstantReadNode | MissingNode) def child deprecated("name", "name_loc") - if name + if (name = self.name) ConstantReadNode.new(source, -1, name_loc, 0, name) else MissingNode.new(source, -1, location, 0) @@ -257,11 +338,15 @@ def child class ConstantTargetNode < Node # Returns the list of parts for the full name of this constant. # For example: [:Foo] + #-- + #: () -> Array[Symbol] def full_name_parts [name] end # Returns the full name of this constant. For example: "Foo" + #-- + #: () -> String def full_name name.to_s end @@ -269,6 +354,8 @@ def full_name class ParametersNode < Node # Mirrors the Method#parameters method. + #-- + #: () -> Array[[Symbol, Symbol] | [Symbol]] def signature names = [] #: Array[[Symbol, Symbol] | [Symbol]] @@ -278,7 +365,7 @@ def signature optionals.each { |param| names << [:opt, param.name] } - if rest && rest.is_a?(RestParameterNode) + if (rest = self.rest).is_a?(RestParameterNode) names << [:rest, rest.name || :*] end @@ -309,7 +396,7 @@ def signature keyopt.each { |param| names << [:key, param.name] } - case keyword_rest + case (keyword_rest = self.keyword_rest) when ForwardingParameterNode names.concat([[:rest, :*], [:keyrest, :**], [:block, :&]]) when KeywordRestParameterNode @@ -318,7 +405,7 @@ def signature names << [:nokey] end - case block + case (block = self.block) when BlockParameterNode names << [:block, block.name || :&] when NoBlockParameterNode @@ -339,6 +426,8 @@ class CallNode < Node # can be any amount of space between the message and the = sign. However, # sometimes you want the location of the full message including the inner # space and the = sign. This method provides that. + #-- + #: () -> Location? def full_message_loc attribute_write? ? message_loc&.adjoin("=") : message_loc end @@ -347,6 +436,8 @@ def full_message_loc class CallOperatorWriteNode < Node # Returns the binary operator used to modify the receiver. This method is # deprecated in favor of #binary_operator. + #-- + #: () -> Symbol def operator deprecated("binary_operator") binary_operator @@ -354,6 +445,8 @@ def operator # Returns the location of the binary operator used to modify the receiver. # This method is deprecated in favor of #binary_operator_loc. + #-- + #: () -> Location def operator_loc deprecated("binary_operator_loc") binary_operator_loc @@ -363,6 +456,8 @@ def operator_loc class ClassVariableOperatorWriteNode < Node # Returns the binary operator used to modify the receiver. This method is # deprecated in favor of #binary_operator. + #-- + #: () -> Symbol def operator deprecated("binary_operator") binary_operator @@ -370,6 +465,8 @@ def operator # Returns the location of the binary operator used to modify the receiver. # This method is deprecated in favor of #binary_operator_loc. + #-- + #: () -> Location def operator_loc deprecated("binary_operator_loc") binary_operator_loc @@ -379,6 +476,8 @@ def operator_loc class ConstantOperatorWriteNode < Node # Returns the binary operator used to modify the receiver. This method is # deprecated in favor of #binary_operator. + #-- + #: () -> Symbol def operator deprecated("binary_operator") binary_operator @@ -386,6 +485,8 @@ def operator # Returns the location of the binary operator used to modify the receiver. # This method is deprecated in favor of #binary_operator_loc. + #-- + #: () -> Location def operator_loc deprecated("binary_operator_loc") binary_operator_loc @@ -395,6 +496,8 @@ def operator_loc class ConstantPathOperatorWriteNode < Node # Returns the binary operator used to modify the receiver. This method is # deprecated in favor of #binary_operator. + #-- + #: () -> Symbol def operator deprecated("binary_operator") binary_operator @@ -402,6 +505,8 @@ def operator # Returns the location of the binary operator used to modify the receiver. # This method is deprecated in favor of #binary_operator_loc. + #-- + #: () -> Location def operator_loc deprecated("binary_operator_loc") binary_operator_loc @@ -411,6 +516,8 @@ def operator_loc class GlobalVariableOperatorWriteNode < Node # Returns the binary operator used to modify the receiver. This method is # deprecated in favor of #binary_operator. + #-- + #: () -> Symbol def operator deprecated("binary_operator") binary_operator @@ -418,6 +525,8 @@ def operator # Returns the location of the binary operator used to modify the receiver. # This method is deprecated in favor of #binary_operator_loc. + #-- + #: () -> Location def operator_loc deprecated("binary_operator_loc") binary_operator_loc @@ -427,6 +536,8 @@ def operator_loc class IndexOperatorWriteNode < Node # Returns the binary operator used to modify the receiver. This method is # deprecated in favor of #binary_operator. + #-- + #: () -> Symbol def operator deprecated("binary_operator") binary_operator @@ -434,6 +545,8 @@ def operator # Returns the location of the binary operator used to modify the receiver. # This method is deprecated in favor of #binary_operator_loc. + #-- + #: () -> Location def operator_loc deprecated("binary_operator_loc") binary_operator_loc @@ -443,6 +556,8 @@ def operator_loc class InstanceVariableOperatorWriteNode < Node # Returns the binary operator used to modify the receiver. This method is # deprecated in favor of #binary_operator. + #-- + #: () -> Symbol def operator deprecated("binary_operator") binary_operator @@ -450,6 +565,8 @@ def operator # Returns the location of the binary operator used to modify the receiver. # This method is deprecated in favor of #binary_operator_loc. + #-- + #: () -> Location def operator_loc deprecated("binary_operator_loc") binary_operator_loc @@ -459,6 +576,8 @@ def operator_loc class LocalVariableOperatorWriteNode < Node # Returns the binary operator used to modify the receiver. This method is # deprecated in favor of #binary_operator. + #-- + #: () -> Symbol def operator deprecated("binary_operator") binary_operator @@ -466,6 +585,8 @@ def operator # Returns the location of the binary operator used to modify the receiver. # This method is deprecated in favor of #binary_operator_loc. + #-- + #: () -> Location def operator_loc deprecated("binary_operator_loc") binary_operator_loc @@ -475,6 +596,8 @@ def operator_loc class CaseMatchNode < Node # Returns the else clause of the case match node. This method is deprecated # in favor of #else_clause. + #-- + #: () -> ElseNode? def consequent deprecated("else_clause") else_clause @@ -484,6 +607,8 @@ def consequent class CaseNode < Node # Returns the else clause of the case node. This method is deprecated in # favor of #else_clause. + #-- + #: () -> ElseNode? def consequent deprecated("else_clause") else_clause @@ -493,6 +618,8 @@ def consequent class IfNode < Node # Returns the subsequent if/elsif/else clause of the if node. This method is # deprecated in favor of #subsequent. + #-- + #: () -> (IfNode | ElseNode)? def consequent deprecated("subsequent") subsequent @@ -502,6 +629,8 @@ def consequent class RescueNode < Node # Returns the subsequent rescue clause of the rescue node. This method is # deprecated in favor of #subsequent. + #-- + #: () -> RescueNode? def consequent deprecated("subsequent") subsequent @@ -511,6 +640,8 @@ def consequent class UnlessNode < Node # Returns the else clause of the unless node. This method is deprecated in # favor of #else_clause. + #-- + #: () -> ElseNode? def consequent deprecated("else_clause") else_clause diff --git a/lib/prism/parse_result.rb b/lib/prism/parse_result.rb index 07529c4295cc87..e2e1145bdc60b9 100644 --- a/lib/prism/parse_result.rb +++ b/lib/prism/parse_result.rb @@ -1,7 +1,15 @@ # frozen_string_literal: true +# rbs_inline: enabled # :markup: markdown module Prism + # @rbs! + # # An internal interface for a cache that can be used to compute code + # # units from byte offsets. + # interface _CodeUnitsCache + # def []: (Integer byte_offset) -> Integer + # end + # This represents a source of Ruby code that has been parsed. It is used in # conjunction with locations to allow them to resolve line numbers and source # ranges. @@ -10,6 +18,8 @@ class Source # be used instead of `new` and it will return either a `Source` or a # specialized and more performant `ASCIISource` if no multibyte characters # are present in the source code. + #-- + #: (String source, ?Integer start_line, ?Array[Integer] offsets) -> Source def self.for(source, start_line = 1, offsets = []) if source.ascii_only? ASCIISource.new(source, start_line, offsets) @@ -34,15 +44,17 @@ def self.for(source, start_line = 1, offsets = []) end # The source code that this source object represents. - attr_reader :source + attr_reader :source #: String # The line number where this source starts. - attr_reader :start_line + attr_reader :start_line #: Integer # The list of newline byte offsets in the source code. - attr_reader :offsets + attr_reader :offsets #: Array[Integer] # Create a new source object with the given source code. + #-- + #: (String source, ?Integer start_line, ?Array[Integer] offsets) -> void def initialize(source, start_line = 1, offsets = []) @source = source @start_line = start_line # set after parsing is done @@ -50,33 +62,45 @@ def initialize(source, start_line = 1, offsets = []) end # Replace the value of start_line with the given value. + #-- + #: (Integer start_line) -> void def replace_start_line(start_line) @start_line = start_line end # Replace the value of offsets with the given value. + #-- + #: (Array[Integer] offsets) -> void def replace_offsets(offsets) @offsets.replace(offsets) end # Returns the encoding of the source code, which is set by parameters to the # parser or by the encoding magic comment. + #-- + #: () -> Encoding def encoding source.encoding end # Returns the lines of the source code as an array of strings. + #-- + #: () -> Array[String] def lines source.lines end # Perform a byteslice on the source code using the given byte offset and # byte length. + #-- + #: (Integer byte_offset, Integer length) -> String def slice(byte_offset, length) source.byteslice(byte_offset, length) or raise end # Converts the line number and column in bytes to a byte offset. + #-- + #: (Integer line, Integer column) -> Integer def byte_offset(line, column) normal = line - @start_line raise IndexError if normal < 0 @@ -87,33 +111,45 @@ def byte_offset(line, column) # Binary search through the offsets to find the line number for the given # byte offset. + #-- + #: (Integer byte_offset) -> Integer def line(byte_offset) start_line + find_line(byte_offset) end # Return the byte offset of the start of the line corresponding to the given # byte offset. + #-- + #: (Integer byte_offset) -> Integer def line_start(byte_offset) offsets[find_line(byte_offset)] end # Returns the byte offset of the end of the line corresponding to the given # byte offset. + #-- + #: (Integer byte_offset) -> Integer def line_end(byte_offset) offsets[find_line(byte_offset) + 1] || source.bytesize end # Return the column in bytes for the given byte offset. + #-- + #: (Integer byte_offset) -> Integer def column(byte_offset) byte_offset - line_start(byte_offset) end # Return the character offset for the given byte offset. + #-- + #: (Integer byte_offset) -> Integer def character_offset(byte_offset) (source.byteslice(0, byte_offset) or raise).length end # Return the column in characters for the given byte offset. + #-- + #: (Integer byte_offset) -> Integer def character_column(byte_offset) character_offset(byte_offset) - character_offset(line_start(byte_offset)) end @@ -130,6 +166,8 @@ def character_column(byte_offset) # possible that the given byte offset will not occur on a character # boundary. Second, it's possible that the source code will contain a # character that has no equivalent in the given encoding. + #-- + #: (Integer byte_offset, Encoding encoding) -> Integer def code_units_offset(byte_offset, encoding) byteslice = (source.byteslice(0, byte_offset) or raise).encode(encoding, invalid: :replace, undef: :replace) @@ -142,17 +180,23 @@ def code_units_offset(byte_offset, encoding) # Generate a cache that targets a specific encoding for calculating code # unit offsets. + #-- + #: (Encoding encoding) -> CodeUnitsCache def code_units_cache(encoding) CodeUnitsCache.new(source, encoding) end # Returns the column in code units for the given encoding for the # given byte offset. + #-- + #: (Integer byte_offset, Encoding encoding) -> Integer def code_units_column(byte_offset, encoding) code_units_offset(byte_offset, encoding) - code_units_offset(line_start(byte_offset), encoding) end # Freeze this object and the objects it contains. + #-- + #: () -> void def deep_freeze source.freeze offsets.freeze @@ -163,6 +207,8 @@ def deep_freeze # Binary search through the offsets to find the line number for the given # byte offset. + #-- + #: (Integer byte_offset) -> Integer def find_line(byte_offset) # :nodoc: index = offsets.bsearch_index { |offset| offset > byte_offset } || offsets.length index - 1 @@ -185,30 +231,47 @@ def find_line(byte_offset) # :nodoc: # class CodeUnitsCache class UTF16Counter # :nodoc: + # @rbs @source: String + # @rbs @encoding: Encoding + + #: (String source, Encoding encoding) -> void def initialize(source, encoding) @source = source @encoding = encoding end + #: (Integer byte_offset, Integer byte_length) -> Integer def count(byte_offset, byte_length) - @source.byteslice(byte_offset, byte_length).encode(@encoding, invalid: :replace, undef: :replace).bytesize / 2 + (@source.byteslice(byte_offset, byte_length) or raise).encode(@encoding, invalid: :replace, undef: :replace).bytesize / 2 end end class LengthCounter # :nodoc: + # @rbs @source: String + # @rbs @encoding: Encoding + + #: (String source, Encoding encoding) -> void def initialize(source, encoding) @source = source @encoding = encoding end + #: (Integer byte_offset, Integer byte_length) -> Integer def count(byte_offset, byte_length) - @source.byteslice(byte_offset, byte_length).encode(@encoding, invalid: :replace, undef: :replace).length + (@source.byteslice(byte_offset, byte_length) or raise).encode(@encoding, invalid: :replace, undef: :replace).length end end private_constant :UTF16Counter, :LengthCounter + # @rbs @source: String + # @rbs @counter: UTF16Counter | LengthCounter + # @rbs @cache: Hash[Integer, Integer] + # @rbs @offsets: Array[Integer] + # Initialize a new cache with the given source and encoding. + #-- + #: (String source, Encoding encoding) -> void def initialize(source, encoding) @source = source @counter = @@ -223,6 +286,8 @@ def initialize(source, encoding) end # Retrieve the code units offset from the given byte offset. + #-- + #: (Integer byte_offset) -> Integer def [](byte_offset) @cache[byte_offset] ||= if (index = @offsets.bsearch_index { |offset| offset > byte_offset }).nil? @@ -249,11 +314,15 @@ def [](byte_offset) # at that point we will treat everything as single-byte characters. class ASCIISource < Source # Return the character offset for the given byte offset. + #-- + #: (Integer byte_offset) -> Integer def character_offset(byte_offset) byte_offset end # Return the column in characters for the given byte offset. + #-- + #: (Integer byte_offset) -> Integer def character_column(byte_offset) byte_offset - line_start(byte_offset) end @@ -264,6 +333,8 @@ def character_column(byte_offset) # This method is tested with UTF-8, UTF-16, and UTF-32. If there is the # concept of code units that differs from the number of characters in other # encodings, it is not captured here. + #-- + #: (Integer byte_offset, Encoding encoding) -> Integer def code_units_offset(byte_offset, encoding) byte_offset end @@ -271,6 +342,8 @@ def code_units_offset(byte_offset, encoding) # Returns a cache that is the identity function in order to maintain the # same interface. We can do this because code units are always equivalent to # byte offsets for ASCII-only sources. + #-- + #: (Encoding encoding) -> _CodeUnitsCache def code_units_cache(encoding) ->(byte_offset) { byte_offset } end @@ -278,6 +351,8 @@ def code_units_cache(encoding) # Specialized version of `code_units_column` that does not depend on # `code_units_offset`, which is a more expensive operation. This is # essentially the same as `Prism::Source#column`. + #-- + #: (Integer byte_offset, Encoding encoding) -> Integer def code_units_column(byte_offset, encoding) byte_offset - line_start(byte_offset) end @@ -287,18 +362,23 @@ def code_units_column(byte_offset, encoding) class Location # A Source object that is used to determine more information from the given # offset and length. - attr_reader :source + attr_reader :source #: Source protected :source # The byte offset from the beginning of the source where this location # starts. - attr_reader :start_offset + attr_reader :start_offset #: Integer # The length of this location in bytes. - attr_reader :length + attr_reader :length #: Integer + + # @rbs @leading_comments: Array[Comment]? + # @rbs @trailing_comments: Array[Comment]? # Create a new location object with the given source, start byte offset, and # byte length. + #-- + #: (Source source, Integer start_offset, Integer length) -> void def initialize(source, start_offset, length) @source = source @start_offset = start_offset @@ -313,53 +393,73 @@ def initialize(source, start_offset, length) # These are the comments that are associated with this location that exist # before the start of this location. + #-- + #: () -> Array[Comment] def leading_comments @leading_comments ||= [] end # Attach a comment to the leading comments of this location. + #-- + #: (Comment comment) -> void def leading_comment(comment) leading_comments << comment end # These are the comments that are associated with this location that exist # after the end of this location. + #-- + #: () -> Array[Comment] def trailing_comments @trailing_comments ||= [] end # Attach a comment to the trailing comments of this location. + #-- + #: (Comment comment) -> void def trailing_comment(comment) trailing_comments << comment end # Returns all comments that are associated with this location (both leading # and trailing comments). + #-- + #: () -> Array[Comment] def comments - [*@leading_comments, *@trailing_comments] + [*@leading_comments, *@trailing_comments] #: Array[Comment] end # Create a new location object with the given options. + #-- + #: (?source: Source, ?start_offset: Integer, ?length: Integer) -> Location def copy(source: self.source, start_offset: self.start_offset, length: self.length) Location.new(source, start_offset, length) end # Returns a new location that is the result of chopping off the last byte. + #-- + #: () -> Location def chop copy(length: length == 0 ? length : length - 1) end # Returns a string representation of this location. + #-- + #: () -> String def inspect # :nodoc: "#" end # Returns all of the lines of the source code associated with this location. + #-- + #: () -> Array[String] def source_lines source.lines end # The source code that this location represents. + #-- + #: () -> String def slice source.slice(start_offset, length) end @@ -367,6 +467,8 @@ def slice # The source code that this location represents starting from the beginning # of the line that this location starts on to the end of the line that this # location ends on. + #-- + #: () -> String def slice_lines line_start = source.line_start(start_offset) line_end = source.line_end(end_offset) @@ -375,118 +477,160 @@ def slice_lines # The character offset from the beginning of the source where this location # starts. + #-- + #: () -> Integer def start_character_offset source.character_offset(start_offset) end # The offset from the start of the file in code units of the given encoding. + #-- + #: (Encoding encoding) -> Integer def start_code_units_offset(encoding = Encoding::UTF_16LE) source.code_units_offset(start_offset, encoding) end # The start offset from the start of the file in code units using the given # cache to fetch or calculate the value. + #-- + #: (_CodeUnitsCache cache) -> Integer def cached_start_code_units_offset(cache) cache[start_offset] end # The byte offset from the beginning of the source where this location ends. + #-- + #: () -> Integer def end_offset start_offset + length end # The character offset from the beginning of the source where this location # ends. + #-- + #: () -> Integer def end_character_offset source.character_offset(end_offset) end # The offset from the start of the file in code units of the given encoding. + #-- + #: (Encoding encoding) -> Integer def end_code_units_offset(encoding = Encoding::UTF_16LE) source.code_units_offset(end_offset, encoding) end # The end offset from the start of the file in code units using the given # cache to fetch or calculate the value. + #-- + #: (_CodeUnitsCache cache) -> Integer def cached_end_code_units_offset(cache) cache[end_offset] end # The line number where this location starts. + #-- + #: () -> Integer def start_line source.line(start_offset) end # The content of the line where this location starts before this location. + #-- + #: () -> String def start_line_slice offset = source.line_start(start_offset) source.slice(offset, start_offset - offset) end # The line number where this location ends. + #-- + #: () -> Integer def end_line source.line(end_offset) end # The column in bytes where this location starts from the start of # the line. + #-- + #: () -> Integer def start_column source.column(start_offset) end # The column in characters where this location ends from the start of # the line. + #-- + #: () -> Integer def start_character_column source.character_column(start_offset) end # The column in code units of the given encoding where this location # starts from the start of the line. + #-- + #: (?Encoding encoding) -> Integer def start_code_units_column(encoding = Encoding::UTF_16LE) source.code_units_column(start_offset, encoding) end # The start column in code units using the given cache to fetch or calculate # the value. + #-- + #: (_CodeUnitsCache cache) -> Integer def cached_start_code_units_column(cache) cache[start_offset] - cache[source.line_start(start_offset)] end # The column in bytes where this location ends from the start of the # line. + #-- + #: () -> Integer def end_column source.column(end_offset) end # The column in characters where this location ends from the start of # the line. + #-- + #: () -> Integer def end_character_column source.character_column(end_offset) end # The column in code units of the given encoding where this location # ends from the start of the line. + #-- + #: (?Encoding encoding) -> Integer def end_code_units_column(encoding = Encoding::UTF_16LE) source.code_units_column(end_offset, encoding) end # The end column in code units using the given cache to fetch or calculate # the value. + #-- + #: (_CodeUnitsCache cache) -> Integer def cached_end_code_units_column(cache) cache[end_offset] - cache[source.line_start(end_offset)] end # Implement the hash pattern matching interface for Location. + #-- + #: (Array[Symbol]? keys) -> Hash[Symbol, untyped] def deconstruct_keys(keys) # :nodoc: { start_offset: start_offset, end_offset: end_offset } end # Implement the pretty print interface for Location. + #-- + #: (PP q) -> void def pretty_print(q) # :nodoc: q.text("(#{start_line},#{start_column})-(#{end_line},#{end_column})") end # Returns true if the given other location is equal to this location. + #-- + #: (untyped other) -> bool def ==(other) Location === other && other.start_offset == start_offset && @@ -496,6 +640,8 @@ def ==(other) # Returns a new location that stretches from this location to the given # other location. Raises an error if this location is not before the other # location or if they don't share the same source. + #-- + #: (Location other) -> Location def join(other) raise "Incompatible sources" if source != other.source raise "Incompatible locations" if start_offset > other.start_offset @@ -506,6 +652,8 @@ def join(other) # Join this location with the first occurrence of the string in the source # that occurs after this location on the same line, and return the new # location. This will raise an error if the string does not exist. + #-- + #: (String string) -> Location def adjoin(string) line_suffix = source.slice(end_offset, source.line_end(end_offset) - end_offset) @@ -520,22 +668,37 @@ def adjoin(string) # base class for all comment types. class Comment # The Location of this comment in the source. - attr_reader :location + attr_reader :location #: Location # Create a new comment object with the given location. + #-- + #: (Location location) -> void def initialize(location) @location = location end # Implement the hash pattern matching interface for Comment. + #-- + #: (Array[Symbol]? keys) -> Hash[Symbol, untyped] def deconstruct_keys(keys) # :nodoc: { location: location } end # Returns the content of the comment by slicing it from the source code. + #-- + #: () -> String def slice location.slice end + + # Returns true if this comment happens on the same line as other code and + # false if the comment is by itself. This can only be true for inline + # comments and should be false for block comments. + #-- + #: () -> bool + def trailing? + raise NotImplementedError, "trailing? is not implemented for #{self.class}" + end end # InlineComment objects are the most common. They correspond to comments in @@ -543,11 +706,15 @@ def slice class InlineComment < Comment # Returns true if this comment happens on the same line as other code and # false if the comment is by itself. + #-- + #: () -> bool def trailing? !location.start_line_slice.strip.empty? end # Returns a string representation of this comment. + #-- + #: () -> String def inspect # :nodoc: "#" end @@ -557,11 +724,15 @@ def inspect # :nodoc: # and =end. class EmbDocComment < Comment # Returns false. This can only be true for inline comments. + #-- + #: () -> bool def trailing? false end # Returns a string representation of this comment. + #-- + #: () -> String def inspect # :nodoc: "#" end @@ -570,33 +741,43 @@ def inspect # :nodoc: # This represents a magic comment that was encountered during parsing. class MagicComment # A Location object representing the location of the key in the source. - attr_reader :key_loc + attr_reader :key_loc #: Location # A Location object representing the location of the value in the source. - attr_reader :value_loc + attr_reader :value_loc #: Location # Create a new magic comment object with the given key and value locations. + #-- + #: (Location key_loc, Location value_loc) -> void def initialize(key_loc, value_loc) @key_loc = key_loc @value_loc = value_loc end # Returns the key of the magic comment by slicing it from the source code. + #-- + #: () -> String def key key_loc.slice end # Returns the value of the magic comment by slicing it from the source code. + #-- + #: () -> String def value value_loc.slice end # Implement the hash pattern matching interface for MagicComment. + #-- + #: (Array[Symbol]? keys) -> Hash[Symbol, untyped] def deconstruct_keys(keys) # :nodoc: { key_loc: key_loc, value_loc: value_loc } end # Returns a string representation of this magic comment. + #-- + #: () -> String def inspect # :nodoc: "#" end @@ -606,18 +787,20 @@ def inspect # :nodoc: class ParseError # The type of error. This is an _internal_ symbol that is used for # communicating with translation layers. It is not meant to be public API. - attr_reader :type + attr_reader :type #: Symbol # The message associated with this error. - attr_reader :message + attr_reader :message #: String # A Location object representing the location of this error in the source. - attr_reader :location + attr_reader :location #: Location # The level of this error. - attr_reader :level + attr_reader :level #: Symbol # Create a new error object with the given message and location. + #-- + #: (Symbol type, String message, Location location, Symbol level) -> void def initialize(type, message, location, level) @type = type @message = message @@ -626,11 +809,15 @@ def initialize(type, message, location, level) end # Implement the hash pattern matching interface for ParseError. + #-- + #: (Array[Symbol]? keys) -> Hash[Symbol, untyped] def deconstruct_keys(keys) # :nodoc: { type: type, message: message, location: location, level: level } end # Returns a string representation of this error. + #-- + #: () -> String def inspect # :nodoc: "#" end @@ -640,18 +827,20 @@ def inspect # :nodoc: class ParseWarning # The type of warning. This is an _internal_ symbol that is used for # communicating with translation layers. It is not meant to be public API. - attr_reader :type + attr_reader :type #: Symbol # The message associated with this warning. - attr_reader :message + attr_reader :message #: String # A Location object representing the location of this warning in the source. - attr_reader :location + attr_reader :location #: Location # The level of this warning. - attr_reader :level + attr_reader :level #: Symbol # Create a new warning object with the given message and location. + #-- + #: (Symbol type, String message, Location location, Symbol level) -> void def initialize(type, message, location, level) @type = type @message = message @@ -660,11 +849,15 @@ def initialize(type, message, location, level) end # Implement the hash pattern matching interface for ParseWarning. + #-- + #: (Array[Symbol]? keys) -> Hash[Symbol, untyped] def deconstruct_keys(keys) # :nodoc: { type: type, message: message, location: location, level: level } end # Returns a string representation of this warning. + #-- + #: () -> String def inspect # :nodoc: "#" end @@ -675,26 +868,28 @@ def inspect # :nodoc: # and any errors that were encountered. class Result # The list of comments that were encountered during parsing. - attr_reader :comments + attr_reader :comments #: Array[Comment] # The list of magic comments that were encountered during parsing. - attr_reader :magic_comments + attr_reader :magic_comments #: Array[MagicComment] # An optional location that represents the location of the __END__ marker # and the rest of the content of the file. This content is loaded into the # DATA constant when the file being parsed is the main file being executed. - attr_reader :data_loc + attr_reader :data_loc #: Location? # The list of errors that were generated during parsing. - attr_reader :errors + attr_reader :errors #: Array[ParseError] # The list of warnings that were generated during parsing. - attr_reader :warnings + attr_reader :warnings #: Array[ParseWarning] # A Source instance that represents the source code that was parsed. - attr_reader :source + attr_reader :source #: Source # Create a new result object with the given values. + #-- + #: (Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, Source source) -> void def initialize(comments, magic_comments, data_loc, errors, warnings, source) @comments = comments @magic_comments = magic_comments @@ -705,28 +900,38 @@ def initialize(comments, magic_comments, data_loc, errors, warnings, source) end # Implement the hash pattern matching interface for Result. + #-- + #: (Array[Symbol]? keys) -> Hash[Symbol, untyped] def deconstruct_keys(keys) # :nodoc: { comments: comments, magic_comments: magic_comments, data_loc: data_loc, errors: errors, warnings: warnings } end # Returns the encoding of the source code that was parsed. + #-- + #: () -> Encoding def encoding source.encoding end # Returns true if there were no errors during parsing and false if there # were. + #-- + #: () -> bool def success? errors.empty? end # Returns true if there were errors during parsing and false if there were # not. + #-- + #: () -> bool def failure? !success? end # Create a code units cache for the given encoding. + #-- + #: (Encoding encoding) -> _CodeUnitsCache def code_units_cache(encoding) source.code_units_cache(encoding) end @@ -743,32 +948,42 @@ class ParseResult < Result private_constant :Newlines # The syntax tree that was parsed from the source code. - attr_reader :value + attr_reader :value #: ProgramNode # Create a new parse result object with the given values. + #-- + #: (ProgramNode value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, Source source) -> void def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) @value = value super(comments, magic_comments, data_loc, errors, warnings, source) end # Implement the hash pattern matching interface for ParseResult. + #-- + #: (Array[Symbol]? keys) -> Hash[Symbol, untyped] def deconstruct_keys(keys) # :nodoc: super.merge!(value: value) end # Attach the list of comments to their respective locations in the tree. + #-- + #: () -> void def attach_comments! Comments.new(self).attach! # steep:ignore end # Walk the tree and mark nodes that are on a new line, loosely emulating # the behavior of CRuby's `:line` tracepoint event. + #-- + #: () -> void def mark_newlines! value.accept(Newlines.new(source.offsets.size)) # steep:ignore end # Returns a string representation of the syntax tree with the errors # displayed inline. + #-- + #: () -> String def errors_format Errors.new(self).format end @@ -777,15 +992,19 @@ def errors_format # This is a result specific to the `lex` and `lex_file` methods. class LexResult < Result # The list of tokens that were parsed from the source code. - attr_reader :value + attr_reader :value #: Array[[Token, Integer]] # Create a new lex result object with the given values. + #-- + #: (Array[[Token, Integer]] value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, Source source) -> void def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) @value = value super(comments, magic_comments, data_loc, errors, warnings, source) end # Implement the hash pattern matching interface for LexResult. + #-- + #: (Array[Symbol]? keys) -> Hash[Symbol, untyped] def deconstruct_keys(keys) # :nodoc: super.merge!(value: value) end @@ -795,15 +1014,19 @@ def deconstruct_keys(keys) # :nodoc: class ParseLexResult < Result # A tuple of the syntax tree and the list of tokens that were parsed from # the source code. - attr_reader :value + attr_reader :value #: [ProgramNode, Array[[Token, Integer]]] # Create a new parse lex result object with the given values. + #-- + #: ([ProgramNode, Array[[Token, Integer]]] value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, Source source) -> void def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) @value = value super(comments, magic_comments, data_loc, errors, warnings, source) end # Implement the hash pattern matching interface for ParseLexResult. + #-- + #: (Array[Symbol]? keys) -> Hash[Symbol, untyped] def deconstruct_keys(keys) # :nodoc: super.merge!(value: value) end @@ -812,16 +1035,20 @@ def deconstruct_keys(keys) # :nodoc: # This represents a token from the Ruby source. class Token # The Source object that represents the source this token came from. - attr_reader :source + attr_reader :source #: Source private :source # The type of token that this token is. - attr_reader :type + attr_reader :type #: Symbol # A byteslice of the source that this token represents. - attr_reader :value + attr_reader :value #: String + + # @rbs @location: Location | Integer # Create a new token object with the given type, value, and location. + #-- + #: (Source source, Symbol type, String value, Location | Integer location) -> void def initialize(source, type, value, location) @source = source @type = type @@ -830,11 +1057,15 @@ def initialize(source, type, value, location) end # Implement the hash pattern matching interface for Token. + #-- + #: (Array[Symbol]? keys) -> Hash[Symbol, untyped] def deconstruct_keys(keys) # :nodoc: { type: type, value: value, location: location } end # A Location object representing the location of this token in the source. + #-- + #: () -> Location def location location = @location return location if location.is_a?(Location) @@ -842,6 +1073,8 @@ def location end # Implement the pretty print interface for Token. + #-- + #: (PP q) -> void def pretty_print(q) # :nodoc: q.group do q.text(type.to_s) @@ -857,6 +1090,8 @@ def pretty_print(q) # :nodoc: end # Returns true if the given other token is equal to this token. + #-- + #: (untyped other) -> bool def ==(other) Token === other && other.type == type && @@ -864,12 +1099,16 @@ def ==(other) end # Returns a string representation of this token. + #-- + #: () -> String def inspect # :nodoc: location super end # Freeze this object and the objects it contains. + #-- + #: () -> void def deep_freeze value.freeze location.freeze @@ -884,14 +1123,16 @@ def deep_freeze class Scope # The list of local variables that are defined in this scope. This should be # defined as an array of symbols. - attr_reader :locals + attr_reader :locals #: Array[Symbol] # The list of local variables that are forwarded to the next scope. This # should by defined as an array of symbols containing the specific values of # :*, :**, :&, or :"...". - attr_reader :forwarding + attr_reader :forwarding #: Array[Symbol] # Create a new scope object with the given locals and forwarding. + #-- + #: (Array[Symbol] locals, Array[Symbol] forwarding) -> void def initialize(locals, forwarding) @locals = locals @forwarding = forwarding @@ -901,6 +1142,8 @@ def initialize(locals, forwarding) # Create a new scope with the given locals and forwarding options that is # suitable for passing into one of the Prism.* methods that accepts the # `scopes` option. + #-- + #: (?locals: Array[Symbol], ?forwarding: Array[Symbol]) -> Scope def self.scope(locals: [], forwarding: []) Scope.new(locals, forwarding) end diff --git a/lib/prism/parse_result/comments.rb b/lib/prism/parse_result/comments.rb index 3e93316aff8662..7b54ce7fd65386 100644 --- a/lib/prism/parse_result/comments.rb +++ b/lib/prism/parse_result/comments.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rbs_inline: enabled # :markup: markdown module Prism @@ -18,32 +19,49 @@ class ParseResult < Result # the comment. Otherwise it will favor attaching to the nearest location # that is after the comment. class Comments + # @rbs! + # # An internal interface for a target that comments can be attached + # # to. This is either going to be a NodeTarget or a CommentTarget. + # interface _CommentTarget + # def start_offset: () -> Integer + # def end_offset: () -> Integer + # def encloses?: (Comment) -> bool + # def leading_comment: (Comment) -> void + # def trailing_comment: (Comment) -> void + # end + # A target for attaching comments that is based on a specific node's # location. class NodeTarget # :nodoc: - attr_reader :node + attr_reader :node #: node + #: (node node) -> void def initialize(node) @node = node end + #: () -> Integer def start_offset node.start_offset end + #: () -> Integer def end_offset node.end_offset end + #: (Comment comment) -> bool def encloses?(comment) start_offset <= comment.location.start_offset && comment.location.end_offset <= end_offset end + #: (Comment comment) -> void def leading_comment(comment) node.location.leading_comment(comment) end + #: (Comment comment) -> void def trailing_comment(comment) node.location.trailing_comment(comment) end @@ -52,44 +70,54 @@ def trailing_comment(comment) # A target for attaching comments that is based on a location field on a # node. For example, the `end` token of a ClassNode. class LocationTarget # :nodoc: - attr_reader :location + attr_reader :location #: Location + #: (Location location) -> void def initialize(location) @location = location end + #: () -> Integer def start_offset location.start_offset end + #: () -> Integer def end_offset location.end_offset end + #: (Comment comment) -> bool def encloses?(comment) false end + #: (Comment comment) -> void def leading_comment(comment) location.leading_comment(comment) end + #: (Comment comment) -> void def trailing_comment(comment) location.trailing_comment(comment) end end # The parse result that we are attaching comments to. - attr_reader :parse_result + attr_reader :parse_result #: ParseResult # Create a new Comments object that will attach comments to the given # parse result. + #-- + #: (ParseResult parse_result) -> void def initialize(parse_result) @parse_result = parse_result end # Attach the comments to their respective locations in the tree by # mutating the parse result. + #-- + #: () -> void def attach! parse_result.comments.each do |comment| preceding, enclosing, following = nearest_targets(parse_result.value, comment) @@ -117,11 +145,13 @@ def attach! # Responsible for finding the nearest targets to the given comment within # the context of the given encapsulating node. + #-- + #: (node node, Comment comment) -> [_CommentTarget?, _CommentTarget?, _CommentTarget?] def nearest_targets(node, comment) comment_start = comment.location.start_offset comment_end = comment.location.end_offset - targets = [] #: Array[_Target] + targets = [] #: Array[_CommentTarget] node.comment_targets.map do |value| case value when StatementsNode @@ -134,8 +164,8 @@ def nearest_targets(node, comment) end targets.sort_by!(&:start_offset) - preceding = nil #: _Target? - following = nil #: _Target? + preceding = nil #: _CommentTarget? + following = nil #: _CommentTarget? left = 0 right = targets.length diff --git a/lib/prism/parse_result/errors.rb b/lib/prism/parse_result/errors.rb index 26c376b3ce895f..03d65daecf0552 100644 --- a/lib/prism/parse_result/errors.rb +++ b/lib/prism/parse_result/errors.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rbs_inline: enabled # :markup: markdown require "stringio" @@ -9,14 +10,18 @@ class ParseResult < Result # can be used to format the errors in a human-readable way. class Errors # The parse result that contains the errors. - attr_reader :parse_result + attr_reader :parse_result #: ParseResult # Initialize a new set of errors from the given parse result. + #-- + #: (ParseResult parse_result) -> void def initialize(parse_result) @parse_result = parse_result end # Formats the errors in a human-readable way and return them as a string. + #-- + #: () -> String def format error_lines = {} #: Hash[Integer, Array[ParseError]] parse_result.errors.each do |error| diff --git a/lib/prism/parse_result/newlines.rb b/lib/prism/parse_result/newlines.rb index e7fd62cafedb37..cfbc1ea1d0e97f 100644 --- a/lib/prism/parse_result/newlines.rb +++ b/lib/prism/parse_result/newlines.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rbs_inline: enabled # :markup: markdown module Prism @@ -24,13 +25,20 @@ class ParseResult < Result # that case. We do that to avoid storing the extra `@newline` instance # variable on every node if we don't need it. class Newlines < Visitor + # The map of lines indices to whether or not they have been marked as + # emitting a newline event. + # @rbs @lines: Array[bool] + # Create a new Newlines visitor with the given newline offsets. + #-- + #: (Integer lines) -> void def initialize(lines) - # @type var lines: Integer @lines = Array.new(1 + lines, false) end - # Permit block/lambda nodes to mark newlines within themselves. + # Permit block nodes to mark newlines within themselves. + #-- + #: (BlockNode node) -> void def visit_block_node(node) old_lines = @lines @lines = Array.new(old_lines.size, false) @@ -42,17 +50,39 @@ def visit_block_node(node) end end - alias_method :visit_lambda_node, :visit_block_node + # Permit lambda nodes to mark newlines within themselves. + #-- + #: (LambdaNode node) -> void + def visit_lambda_node(node) + old_lines = @lines + @lines = Array.new(old_lines.size, false) + + begin + super(node) + ensure + @lines = old_lines + end + end - # Mark if/unless nodes as newlines. + # Mark if nodes as newlines. + #-- + #: (IfNode node) -> void def visit_if_node(node) node.newline_flag!(@lines) super(node) end - alias_method :visit_unless_node, :visit_if_node + # Mark unless nodes as newlines. + #-- + #: (UnlessNode node) -> void + def visit_unless_node(node) + node.newline_flag!(@lines) + super(node) + end # Permit statements lists to mark newlines within themselves. + #-- + #: (StatementsNode node) -> void def visit_statements_node(node) node.body.each do |child| child.newline_flag!(@lines) @@ -63,10 +93,16 @@ def visit_statements_node(node) end class Node + # Tracks whether or not this node should emit a newline event when the + # instructions that it represents are executed. + # @rbs @newline_flag: bool + + #: () -> bool def newline_flag? # :nodoc: !!defined?(@newline_flag) end + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: line = location.start_line unless lines[line] @@ -77,48 +113,56 @@ def newline_flag!(lines) # :nodoc: end class BeginNode < Node + # @rbs override def newline_flag!(lines) # :nodoc: # Never mark BeginNode with a newline flag, mark children instead. end end class ParenthesesNode < Node + # @rbs override def newline_flag!(lines) # :nodoc: # Never mark ParenthesesNode with a newline flag, mark children instead. end end class IfNode < Node + # @rbs override def newline_flag!(lines) # :nodoc: predicate.newline_flag!(lines) end end class UnlessNode < Node + # @rbs override def newline_flag!(lines) # :nodoc: predicate.newline_flag!(lines) end end class UntilNode < Node + # @rbs override def newline_flag!(lines) # :nodoc: predicate.newline_flag!(lines) end end class WhileNode < Node + # @rbs override def newline_flag!(lines) # :nodoc: predicate.newline_flag!(lines) end end class RescueModifierNode < Node + # @rbs override def newline_flag!(lines) # :nodoc: expression.newline_flag!(lines) end end class InterpolatedMatchLastLineNode < Node + # @rbs override def newline_flag!(lines) # :nodoc: first = parts.first first.newline_flag!(lines) if first @@ -126,6 +170,7 @@ def newline_flag!(lines) # :nodoc: end class InterpolatedRegularExpressionNode < Node + # @rbs override def newline_flag!(lines) # :nodoc: first = parts.first first.newline_flag!(lines) if first @@ -133,6 +178,7 @@ def newline_flag!(lines) # :nodoc: end class InterpolatedStringNode < Node + # @rbs override def newline_flag!(lines) # :nodoc: first = parts.first first.newline_flag!(lines) if first @@ -140,6 +186,7 @@ def newline_flag!(lines) # :nodoc: end class InterpolatedSymbolNode < Node + # @rbs override def newline_flag!(lines) # :nodoc: first = parts.first first.newline_flag!(lines) if first @@ -147,6 +194,7 @@ def newline_flag!(lines) # :nodoc: end class InterpolatedXStringNode < Node + # @rbs override def newline_flag!(lines) # :nodoc: first = parts.first first.newline_flag!(lines) if first diff --git a/lib/prism/pattern.rb b/lib/prism/pattern.rb index dde9d3b6f96b06..b15b04d9bc97c0 100644 --- a/lib/prism/pattern.rb +++ b/lib/prism/pattern.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rbs_inline: enabled # :markup: markdown module Prism @@ -41,6 +42,8 @@ class Pattern class CompilationError < StandardError # Create a new CompilationError with the given representation of the node # that caused the error. + #-- + #: (String repr) -> void def initialize(repr) # :nodoc: super(<<~ERROR) prism was unable to compile the pattern you provided into a usable @@ -57,10 +60,13 @@ def initialize(repr) # :nodoc: end # The query that this pattern was initialized with. - attr_reader :query + attr_reader :query #: String + # @rbs @compiled: Proc? # Create a new pattern with the given query. The query should be a string # containing a Ruby pattern matching expression. + #-- + #: (String query) -> void def initialize(query) @query = query @compiled = nil @@ -68,6 +74,8 @@ def initialize(query) # Compile the query into a callable object that can be used to match against # nodes. + #-- + #: () -> Proc def compile result = Prism.parse("case nil\nin #{query}\nend") @@ -84,6 +92,9 @@ def compile # pattern. If a block is given, it will be called with each node that # matches the pattern. If no block is given, an enumerator will be returned # that will yield each node that matches the pattern. + #-- + #: (node root) { (node) -> void } -> void + #: (node root) -> Enumerator[node, void] def scan(root) return to_enum(:scan, root) unless block_given? @@ -100,22 +111,32 @@ def scan(root) # Shortcut for combining two procs into one that returns true if both return # true. + #-- + #: (Proc left, Proc right) -> Proc def combine_and(left, right) # :nodoc: ->(other) { left.call(other) && right.call(other) } end # Shortcut for combining two procs into one that returns true if either # returns true. + #-- + #: (Proc left, Proc right) -> Proc def combine_or(left, right) # :nodoc: ->(other) { left.call(other) || right.call(other) } end - # Raise an error because the given node is not supported. + # Raise an error because the given node is not supported. Note purposefully + # not typing this method since it is a no return method that Steep does not + # understand. + #-- + #: (node node) -> bot def compile_error(node) # :nodoc: raise CompilationError, node.inspect end # in [foo, bar, baz] + #-- + #: (ArrayPatternNode node) -> Proc def compile_array_pattern_node(node) # :nodoc: compile_error(node) if !node.rest.nil? || node.posts.any? @@ -141,11 +162,15 @@ def compile_array_pattern_node(node) # :nodoc: end # in foo | bar + #-- + #: (AlternationPatternNode node) -> Proc def compile_alternation_pattern_node(node) # :nodoc: combine_or(compile_node(node.left), compile_node(node.right)) end # in Prism::ConstantReadNode + #-- + #: (ConstantPathNode node) -> Proc def compile_constant_path_node(node) # :nodoc: parent = node.parent @@ -161,11 +186,15 @@ def compile_constant_path_node(node) # :nodoc: # in ConstantReadNode # in String + #-- + #: (ConstantReadNode node) -> Proc def compile_constant_read_node(node) # :nodoc: compile_constant_name(node, node.name) end # Compile a name associated with a constant. + #-- + #: ((ConstantPathNode | ConstantReadNode) node, Symbol name) -> Proc def compile_constant_name(node, name) # :nodoc: if Prism.const_defined?(name, false) clazz = Prism.const_get(name) @@ -182,9 +211,14 @@ def compile_constant_name(node, name) # :nodoc: # in InstanceVariableReadNode[name: Symbol] # in { name: Symbol } + #-- + #: (HashPatternNode node) -> Proc def compile_hash_pattern_node(node) # :nodoc: compile_error(node) if node.rest - compiled_constant = compile_node(node.constant) if node.constant + + if (constant = node.constant) + compiled_constant = compile_node(constant) + end preprocessed = node.elements.to_h do |element| @@ -212,11 +246,15 @@ def compile_hash_pattern_node(node) # :nodoc: end # in nil + #-- + #: (NilNode node) -> Proc def compile_nil_node(node) # :nodoc: ->(attribute) { attribute.nil? } end # in /foo/ + #-- + #: (RegularExpressionNode node) -> Proc def compile_regular_expression_node(node) # :nodoc: regexp = Regexp.new(node.unescaped, node.closing[1..]) @@ -225,6 +263,8 @@ def compile_regular_expression_node(node) # :nodoc: # in "" # in "foo" + #-- + #: (StringNode node) -> Proc def compile_string_node(node) # :nodoc: string = node.unescaped @@ -233,6 +273,8 @@ def compile_string_node(node) # :nodoc: # in :+ # in :foo + #-- + #: (SymbolNode node) -> Proc def compile_symbol_node(node) # :nodoc: symbol = node.unescaped.to_sym @@ -241,6 +283,8 @@ def compile_symbol_node(node) # :nodoc: # Compile any kind of node. Dispatch out to the individual compilation # methods based on the type of node. + #-- + #: (node node) -> Proc def compile_node(node) # :nodoc: case node when AlternationPatternNode diff --git a/lib/prism/prism.gemspec b/lib/prism/prism.gemspec index dde7e711e2e580..f3cba41a4338bf 100644 --- a/lib/prism/prism.gemspec +++ b/lib/prism/prism.gemspec @@ -123,24 +123,28 @@ Gem::Specification.new do |spec| "rbi/prism/translation/parser_versions.rbi", "rbi/prism/translation/ripper.rbi", "rbi/prism/visitor.rbi", - "sig/prism.rbs", - "sig/prism/compiler.rbs", - "sig/prism/dispatcher.rbs", - "sig/prism/dot_visitor.rbs", - "sig/prism/dsl.rbs", - "sig/prism/inspect_visitor.rbs", - "sig/prism/lex_compat.rbs", - "sig/prism/mutation_compiler.rbs", - "sig/prism/node_ext.rbs", - "sig/prism/node.rbs", - "sig/prism/parse_result.rbs", - "sig/prism/parse_result/comments.rbs", - "sig/prism/pattern.rbs", - "sig/prism/reflection.rbs", - "sig/prism/relocation.rbs", - "sig/prism/serialize.rbs", - "sig/prism/string_query.rbs", - "sig/prism/visitor.rbs", + "sig/generated/prism.rbs", + "sig/generated/prism/compiler.rbs", + "sig/generated/prism/desugar_compiler.rbs", + "sig/generated/prism/dispatcher.rbs", + "sig/generated/prism/dot_visitor.rbs", + "sig/generated/prism/dsl.rbs", + "sig/generated/prism/inspect_visitor.rbs", + "sig/generated/prism/lex_compat.rbs", + "sig/generated/prism/mutation_compiler.rbs", + "sig/generated/prism/node.rbs", + "sig/generated/prism/node_ext.rbs", + "sig/generated/prism/parse_result.rbs", + "sig/generated/prism/pattern.rbs", + "sig/generated/prism/reflection.rbs", + "sig/generated/prism/relocation.rbs", + "sig/generated/prism/serialize.rbs", + "sig/generated/prism/string_query.rbs", + "sig/generated/prism/translation.rbs", + "sig/generated/prism/visitor.rbs", + "sig/generated/prism/parse_result/comments.rbs", + "sig/generated/prism/parse_result/errors.rbs", + "sig/generated/prism/parse_result/newlines.rbs", "src/diagnostic.c", "src/encoding.c", "src/node.c", diff --git a/lib/prism/relocation.rb b/lib/prism/relocation.rb index 3e9210a7853431..2ac471d4250ebc 100644 --- a/lib/prism/relocation.rb +++ b/lib/prism/relocation.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rbs_inline: enabled # :markup: markdown module Prism @@ -12,6 +13,33 @@ module Prism # "save" nodes and locations using a minimal amount of memory (just the # node_id and a field identifier) and then reify them later. module Relocation + # @rbs! + # type entry_value = untyped + # type entry_values = Hash[Symbol, entry_value] + # + # interface _Value + # def start_line: () -> Integer + # def end_line: () -> Integer + # def start_offset: () -> Integer + # def end_offset: () -> Integer + # def start_character_offset: () -> Integer + # def end_character_offset: () -> Integer + # def cached_start_code_units_offset: (_CodeUnitsCache cache) -> Integer + # def cached_end_code_units_offset: (_CodeUnitsCache cache) -> Integer + # def start_column: () -> Integer + # def end_column: () -> Integer + # def start_character_column: () -> Integer + # def end_character_column: () -> Integer + # def cached_start_code_units_column: (_CodeUnitsCache cache) -> Integer + # def cached_end_code_units_column: (_CodeUnitsCache cache) -> Integer + # def leading_comments: () -> Array[Comment] + # def trailing_comments: () -> Array[Comment] + # end + # + # interface _Field + # def fields: (_Value value) -> entry_values + # end + # An entry in a repository that will lazily reify its values when they are # first accessed. class Entry @@ -21,109 +49,152 @@ class Entry class MissingValueError < StandardError end + # @rbs @repository: Repository? + # @rbs @values: Hash[Symbol, untyped]? + # Initialize a new entry with the given repository. + #-- + #: (Repository repository) -> void def initialize(repository) @repository = repository @values = nil end # Fetch the filepath of the value. + #-- + #: () -> String def filepath fetch_value(:filepath) end # Fetch the start line of the value. + #-- + #: () -> Integer def start_line fetch_value(:start_line) end # Fetch the end line of the value. + #-- + #: () -> Integer def end_line fetch_value(:end_line) end # Fetch the start byte offset of the value. + #-- + #: () -> Integer def start_offset fetch_value(:start_offset) end # Fetch the end byte offset of the value. + #-- + #: () -> Integer def end_offset fetch_value(:end_offset) end # Fetch the start character offset of the value. + #-- + #: () -> Integer def start_character_offset fetch_value(:start_character_offset) end # Fetch the end character offset of the value. + #-- + #: () -> Integer def end_character_offset fetch_value(:end_character_offset) end # Fetch the start code units offset of the value, for the encoding that # was configured on the repository. + #-- + #: () -> Integer def start_code_units_offset fetch_value(:start_code_units_offset) end # Fetch the end code units offset of the value, for the encoding that was # configured on the repository. + #-- + #: () -> Integer def end_code_units_offset fetch_value(:end_code_units_offset) end # Fetch the start byte column of the value. + #-- + #: () -> Integer def start_column fetch_value(:start_column) end # Fetch the end byte column of the value. + #-- + #: () -> Integer def end_column fetch_value(:end_column) end # Fetch the start character column of the value. + #-- + #: () -> Integer def start_character_column fetch_value(:start_character_column) end # Fetch the end character column of the value. + #-- + #: () -> Integer def end_character_column fetch_value(:end_character_column) end # Fetch the start code units column of the value, for the encoding that # was configured on the repository. + #-- + #: () -> Integer def start_code_units_column fetch_value(:start_code_units_column) end # Fetch the end code units column of the value, for the encoding that was # configured on the repository. + #-- + #: () -> Integer def end_code_units_column fetch_value(:end_code_units_column) end # Fetch the leading comments of the value. + #-- + #: () -> Array[CommentsField::Comment] def leading_comments fetch_value(:leading_comments) end # Fetch the trailing comments of the value. + #-- + #: () -> Array[CommentsField::Comment] def trailing_comments fetch_value(:trailing_comments) end # Fetch the leading and trailing comments of the value. + #-- + #: () -> Array[CommentsField::Comment] def comments - leading_comments.concat(trailing_comments) + [*leading_comments, *trailing_comments] end # Reify the values on this entry with the given values. This is an # internal-only API that is called from the repository when it is time to # reify the values. + #-- + #: (entry_values values) -> void def reify!(values) # :nodoc: @repository = nil @values = values @@ -132,6 +203,8 @@ def reify!(values) # :nodoc: private # Fetch a value from the entry, raising an error if it is missing. + #-- + #: (Symbol name) -> entry_value def fetch_value(name) values.fetch(name) do raise MissingValueError, "No value for #{name}, make sure the " \ @@ -140,27 +213,35 @@ def fetch_value(name) end # Return the values from the repository, reifying them if necessary. + #-- + #: () -> entry_values def values - @values || (@repository.reify!; @values) + @values || (@repository&.reify!; @values) #: entry_values end end # Represents the source of a repository that will be reparsed. class Source # The value that will need to be reparsed. - attr_reader :value + attr_reader :value #: untyped # Initialize the source with the given value. + #-- + #: (untyped value) -> void def initialize(value) @value = value end # Reparse the value and return the parse result. + #-- + #: () -> ParseResult def result raise NotImplementedError, "Subclasses must implement #result" end # Create a code units cache for the given encoding. + #-- + #: (Encoding encoding) -> _CodeUnitsCache def code_units_cache(encoding) result.code_units_cache(encoding) end @@ -169,6 +250,8 @@ def code_units_cache(encoding) # A source that is represented by a file path. class SourceFilepath < Source # Reparse the file and return the parse result. + #-- + #: () -> ParseResult def result Prism.parse_file(value) end @@ -177,6 +260,8 @@ def result # A source that is represented by a string. class SourceString < Source # Reparse the string and return the parse result. + #-- + #: () -> ParseResult def result Prism.parse(value) end @@ -185,14 +270,18 @@ def result # A field that represents the file path. class FilepathField # The file path that this field represents. - attr_reader :value + attr_reader :value #: String # Initialize a new field with the given file path. + #-- + #: (String value) -> void def initialize(value) @value = value end # Fetch the file path. + #-- + #: (_Value _value) -> entry_values def fields(_value) { filepath: value } end @@ -201,6 +290,8 @@ def fields(_value) # A field representing the start and end lines. class LinesField # Fetches the start and end line of a value. + #-- + #: (_Value value) -> entry_values def fields(value) { start_line: value.start_line, end_line: value.end_line } end @@ -209,6 +300,8 @@ def fields(value) # A field representing the start and end byte offsets. class OffsetsField # Fetches the start and end byte offset of a value. + #-- + #: (_Value value) -> entry_values def fields(value) { start_offset: value.start_offset, end_offset: value.end_offset } end @@ -217,6 +310,8 @@ def fields(value) # A field representing the start and end character offsets. class CharacterOffsetsField # Fetches the start and end character offset of a value. + #-- + #: (_Value value) -> entry_values def fields(value) { start_character_offset: value.start_character_offset, @@ -229,12 +324,16 @@ def fields(value) class CodeUnitOffsetsField # A pointer to the repository object that is used for lazily creating a # code units cache. - attr_reader :repository + attr_reader :repository #: Repository # The associated encoding for the code units. - attr_reader :encoding + attr_reader :encoding #: Encoding + + # @rbs @cache: _CodeUnitsCache? # Initialize a new field with the associated repository and encoding. + #-- + #: (Repository repository, Encoding encoding) -> void def initialize(repository, encoding) @repository = repository @encoding = encoding @@ -243,6 +342,8 @@ def initialize(repository, encoding) # Fetches the start and end code units offset of a value for a particular # encoding. + #-- + #: (_Value value) -> entry_values def fields(value) { start_code_units_offset: value.cached_start_code_units_offset(cache), @@ -253,6 +354,8 @@ def fields(value) private # Lazily create a code units cache for the associated encoding. + #-- + #: () -> _CodeUnitsCache def cache @cache ||= repository.code_units_cache(encoding) end @@ -261,6 +364,8 @@ def cache # A field representing the start and end byte columns. class ColumnsField # Fetches the start and end byte column of a value. + #-- + #: (_Value value) -> entry_values def fields(value) { start_column: value.start_column, end_column: value.end_column } end @@ -269,6 +374,8 @@ def fields(value) # A field representing the start and end character columns. class CharacterColumnsField # Fetches the start and end character column of a value. + #-- + #: (_Value value) -> entry_values def fields(value) { start_character_column: value.start_character_column, @@ -282,12 +389,16 @@ def fields(value) class CodeUnitColumnsField # The repository object that is used for lazily creating a code units # cache. - attr_reader :repository + attr_reader :repository #: Repository # The associated encoding for the code units. - attr_reader :encoding + attr_reader :encoding #: Encoding + + # @rbs @cache: _CodeUnitsCache? # Initialize a new field with the associated repository and encoding. + #-- + #: (Repository repository, Encoding encoding) -> void def initialize(repository, encoding) @repository = repository @encoding = encoding @@ -296,6 +407,8 @@ def initialize(repository, encoding) # Fetches the start and end code units column of a value for a particular # encoding. + #-- + #: (_Value value) -> entry_values def fields(value) { start_code_units_column: value.cached_start_code_units_column(cache), @@ -306,6 +419,8 @@ def fields(value) private # Lazily create a code units cache for the associated encoding. + #-- + #: () -> _CodeUnitsCache def cache @cache ||= repository.code_units_cache(encoding) end @@ -316,9 +431,11 @@ class CommentsField # An object that represents a slice of a comment. class Comment # The slice of the comment. - attr_reader :slice + attr_reader :slice #: String # Initialize a new comment with the given slice. + # + #: (String slice) -> void def initialize(slice) @slice = slice end @@ -327,6 +444,8 @@ def initialize(slice) private # Create comment objects from the given values. + #-- + #: (entry_value values) -> Array[Comment] def comments(values) values.map { |value| Comment.new(value.slice) } end @@ -335,6 +454,8 @@ def comments(values) # A field representing the leading comments. class LeadingCommentsField < CommentsField # Fetches the leading comments of a value. + #-- + #: (_Value value) -> entry_values def fields(value) { leading_comments: comments(value.leading_comments) } end @@ -343,6 +464,8 @@ def fields(value) # A field representing the trailing comments. class TrailingCommentsField < CommentsField # Fetches the trailing comments of a value. + #-- + #: (_Value value) -> entry_values def fields(value) { trailing_comments: comments(value.trailing_comments) } end @@ -358,15 +481,17 @@ class ConfigurationError < StandardError # The source associated with this repository. This will be either a # SourceFilepath (the most common use case) or a SourceString. - attr_reader :source + attr_reader :source #: Source # The fields that have been configured on this repository. - attr_reader :fields + attr_reader :fields #: Hash[Symbol, _Field] # The entries that have been saved on this repository. - attr_reader :entries + attr_reader :entries #: Hash[Integer, Hash[Symbol, Entry]] # Initialize a new repository with the given source. + #-- + #: (Source source) -> void def initialize(source) @source = source @fields = {} @@ -374,69 +499,93 @@ def initialize(source) end # Create a code units cache for the given encoding from the source. + #-- + #: (Encoding encoding) -> _CodeUnitsCache def code_units_cache(encoding) source.code_units_cache(encoding) end # Configure the filepath field for this repository and return self. + #-- + #: () -> self def filepath raise ConfigurationError, "Can only specify filepath for a filepath source" unless source.is_a?(SourceFilepath) field(:filepath, FilepathField.new(source.value)) end # Configure the lines field for this repository and return self. + #-- + #: () -> self def lines field(:lines, LinesField.new) end # Configure the offsets field for this repository and return self. + #-- + #: () -> self def offsets field(:offsets, OffsetsField.new) end # Configure the character offsets field for this repository and return # self. + #-- + #: () -> self def character_offsets field(:character_offsets, CharacterOffsetsField.new) end # Configure the code unit offsets field for this repository for a specific # encoding and return self. + #-- + #: (Encoding encoding) -> self def code_unit_offsets(encoding) field(:code_unit_offsets, CodeUnitOffsetsField.new(self, encoding)) end # Configure the columns field for this repository and return self. + #-- + #: () -> self def columns field(:columns, ColumnsField.new) end # Configure the character columns field for this repository and return # self. + #-- + #: () -> self def character_columns field(:character_columns, CharacterColumnsField.new) end # Configure the code unit columns field for this repository for a specific # encoding and return self. + #-- + #: (Encoding encoding) -> self def code_unit_columns(encoding) field(:code_unit_columns, CodeUnitColumnsField.new(self, encoding)) end # Configure the leading comments field for this repository and return # self. + #-- + #: () -> self def leading_comments field(:leading_comments, LeadingCommentsField.new) end # Configure the trailing comments field for this repository and return # self. + #-- + #: () -> self def trailing_comments field(:trailing_comments, TrailingCommentsField.new) end # Configure both the leading and trailing comment fields for this # repository and return self. + #-- + #: () -> self def comments leading_comments.trailing_comments end @@ -444,6 +593,8 @@ def comments # This method is called from nodes and locations when they want to enter # themselves into the repository. It it internal-only and meant to be # called from the #save* APIs. + #-- + #: (Integer node_id, Symbol field_name) -> Entry def enter(node_id, field_name) # :nodoc: entry = Entry.new(self) @entries[node_id][field_name] = entry @@ -453,6 +604,8 @@ def enter(node_id, field_name) # :nodoc: # This method is called from the entries in the repository when they need # to reify their values. It is internal-only and meant to be called from # the various value APIs. + #-- + #: () -> void def reify! # :nodoc: result = source.result @@ -466,7 +619,7 @@ def reify! # :nodoc: while (node = queue.shift) @entries[node.node_id].each do |field_name, entry| value = node.public_send(field_name) - values = {} #: Hash[Symbol, untyped] + values = {} #: entry_values fields.each_value do |field| values.merge!(field.fields(value)) @@ -485,6 +638,8 @@ def reify! # :nodoc: # Append the given field to the repository and return the repository so # that these calls can be chained. + #-- + #: (Symbol name, _Field) -> self def field(name, value) raise ConfigurationError, "Cannot specify multiple #{name} fields" if @fields.key?(name) @fields[name] = value @@ -493,11 +648,15 @@ def field(name, value) end # Create a new repository for the given filepath. + #-- + #: (String value) -> Repository def self.filepath(value) Repository.new(SourceFilepath.new(value)) end # Create a new repository for the given string. + #-- + #: (String value) -> Repository def self.string(value) Repository.new(SourceString.new(value)) end diff --git a/lib/prism/string_query.rb b/lib/prism/string_query.rb index 547f58d2fa6c92..c0dee63d3f9911 100644 --- a/lib/prism/string_query.rb +++ b/lib/prism/string_query.rb @@ -1,29 +1,43 @@ # frozen_string_literal: true +# rbs_inline: enabled # :markup: markdown module Prism # Query methods that allow categorizing strings based on their context for # where they could be valid in a Ruby syntax tree. class StringQuery + # @rbs! + # def self.local?: (String string) -> bool + # def self.constant?: (String string) -> bool + # def self.method_name?: (String string) -> bool + # The string that this query is wrapping. - attr_reader :string + attr_reader :string #: String # Initialize a new query with the given string. + #-- + #: (String string) -> void def initialize(string) @string = string end # Whether or not this string is a valid local variable name. + #-- + #: () -> bool def local? StringQuery.local?(string) end # Whether or not this string is a valid constant name. + #-- + #: () -> bool def constant? StringQuery.constant?(string) end # Whether or not this string is a valid method name. + #-- + #: () -> bool def method_name? StringQuery.method_name?(string) end diff --git a/lib/prism/translation.rb b/lib/prism/translation.rb index 57b57135bcc1f2..d1afa5d8c54a24 100644 --- a/lib/prism/translation.rb +++ b/lib/prism/translation.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rbs_inline: enabled # :markup: markdown module Prism diff --git a/lib/prism/translation/parser_current.rb b/lib/prism/translation/parser_current.rb index f13eff6bbe2d90..2dd018627c2559 100644 --- a/lib/prism/translation/parser_current.rb +++ b/lib/prism/translation/parser_current.rb @@ -2,7 +2,6 @@ # :markup: markdown # typed: ignore -# module Prism module Translation case RUBY_VERSION diff --git a/prism/templates/lib/prism/compiler.rb.erb b/prism/templates/lib/prism/compiler.rb.erb index 031557a221de3c..16035b5456135b 100644 --- a/prism/templates/lib/prism/compiler.rb.erb +++ b/prism/templates/lib/prism/compiler.rb.erb @@ -1,3 +1,5 @@ +# rbs_inline: enabled + module Prism # A compiler is a visitor that returns the value of each node as it visits. # This is as opposed to a visitor which will only walk the tree. This can be @@ -18,22 +20,29 @@ module Prism # class Compiler < Visitor # Visit an individual node. + #-- + #: (node?) -> untyped def visit(node) # :nodoc: node&.accept(self) end # Visit a list of nodes. + #-- + #: (Array[node?]) -> untyped def visit_all(nodes) # :nodoc: nodes.map { |node| node&.accept(self) } end # Visit the child nodes of the given node. + #-- + #: (node) -> Array[untyped] def visit_child_nodes(node) # :nodoc: node.each_child_node.map { |node| node.accept(self) } end <%- nodes.each_with_index do |node, index| -%> <%= "\n" if index != 0 -%> + #: (<%= node.name %>) -> Array[untyped] def visit_<%= node.human %>(node) # :nodoc: node.each_child_node.map { |node| node.accept(self) } end diff --git a/prism/templates/lib/prism/dispatcher.rb.erb b/prism/templates/lib/prism/dispatcher.rb.erb index e4ca84db2421d2..f1bd80a7cdcf2b 100644 --- a/prism/templates/lib/prism/dispatcher.rb.erb +++ b/prism/templates/lib/prism/dispatcher.rb.erb @@ -1,3 +1,5 @@ +# rbs_inline: enabled + module Prism # The dispatcher class fires events for nodes that are found while walking an # AST to all registered listeners. It's useful for performing different types @@ -32,26 +34,35 @@ module Prism # dispatcher.dispatch_once(integer) # class Dispatcher < Visitor - # attr_reader listeners: Hash[Symbol, Array[Listener]] - attr_reader :listeners + # A hash mapping event names to arrays of listeners that should be notified + # when that event is fired. + attr_reader :listeners #: Hash[Symbol, Array[untyped]] # Initialize a new dispatcher. + #-- + #: () -> void def initialize @listeners = {} end # Register a listener for one or more events. + #-- + #: (untyped, *Symbol) -> void def register(listener, *events) register_events(listener, events) end # Register all public methods of a listener that match the pattern # `on__(enter|leave)`. + #-- + #: (untyped) -> void def register_public_methods(listener) register_events(listener, listener.public_methods(false).grep(/\Aon_.+_(?:enter|leave)\z/)) end # Register a listener for the given events. + #-- + #: (untyped, Array[Symbol]) -> void private def register_events(listener, events) # :nodoc: events.each { |event| (listeners[event] ||= []) << listener } end @@ -60,11 +71,14 @@ module Prism alias dispatch visit # Dispatches a single event for `node` to all registered listeners. + #-- + #: (node node) -> void def dispatch_once(node) node.accept(DispatchOnce.new(listeners)) end <%- nodes.each do |node| -%> + #: (<%= node.name %> node) -> void def visit_<%= node.human %>(node) # :nodoc: listeners[:on_<%= node.human %>_enter]&.each { |listener| listener.on_<%= node.human %>_enter(node) } super @@ -73,14 +87,17 @@ module Prism <%- end -%> class DispatchOnce < Visitor # :nodoc: - attr_reader :listeners + attr_reader :listeners #: Hash[Symbol, Array[untyped]] + #: (Hash[Symbol, Array[untyped]] listeners) -> void def initialize(listeners) @listeners = listeners end <%- nodes.each do |node| -%> # Dispatch enter and leave events for <%= node.name %> nodes. + #-- + #: (<%= node.name %> node) -> void def visit_<%= node.human %>(node) listeners[:on_<%= node.human %>_enter]&.each { |listener| listener.on_<%= node.human %>_enter(node) } listeners[:on_<%= node.human %>_leave]&.each { |listener| listener.on_<%= node.human %>_leave(node) } diff --git a/prism/templates/lib/prism/dot_visitor.rb.erb b/prism/templates/lib/prism/dot_visitor.rb.erb index 13c53af0d430d8..3e37cf1a57b2e7 100644 --- a/prism/templates/lib/prism/dot_visitor.rb.erb +++ b/prism/templates/lib/prism/dot_visitor.rb.erb @@ -1,3 +1,5 @@ +# rbs_inline: enabled + require "cgi/escape" require "cgi/util" unless defined?(CGI::EscapeExt) @@ -6,14 +8,18 @@ module Prism # subtree into a graphviz dot graph. class DotVisitor < Visitor class Field # :nodoc: - attr_reader :name, :value, :port + attr_reader :name #: String + attr_reader :value #: String? + attr_reader :port #: bool + #: (String name, String? value, bool port) -> void def initialize(name, value, port) @name = name @value = value @port = port end + #: () -> String def to_dot if port "#{name}" @@ -24,17 +30,21 @@ module Prism end class Table # :nodoc: - attr_reader :name, :fields + attr_reader :name #: String + attr_reader :fields #: Array[Field] + #: (String name) -> void def initialize(name) @name = name @fields = [] end + #: (String name, ?String? value, ?port: bool) -> void def field(name, value = nil, port: false) fields << Field.new(name, value, port) end + #: () -> String def to_dot dot = <<~DOT @@ -50,26 +60,31 @@ module Prism end class Digraph # :nodoc: - attr_reader :nodes, :waypoints, :edges + attr_reader :nodes, :waypoints, :edges #: Array[String] + #: () -> void def initialize @nodes = [] @waypoints = [] @edges = [] end + #: (String value) -> void def node(value) nodes << value end + #: (String value) -> void def waypoint(value) waypoints << value end + #: (String value) -> void def edge(value) edges << value end + #: () -> String def to_dot <<~DOT digraph "Prism" { @@ -93,19 +108,24 @@ module Prism private_constant :Field, :Table, :Digraph # The digraph that is being built. - attr_reader :digraph + attr_reader :digraph #: Digraph # Initialize a new dot visitor. + #-- + #: () -> void def initialize @digraph = Digraph.new end # Convert this visitor into a graphviz dot graph string. + #-- + #: () -> String def to_dot digraph.to_dot end <%- nodes.each do |node| -%> + # (<%= node.name %>) -> void def visit_<%= node.human %>(node) # :nodoc: table = Table.new("<%= node.name %>") id = node_id(node) @@ -151,7 +171,7 @@ module Prism <%- end -%> <%- end -%> - digraph.nodes << <<~DOT + digraph.node(<<~DOT) #{id} [ label=<#{table.to_dot.gsub(/\n/, "\n ")}> ]; @@ -164,11 +184,15 @@ module Prism private # Generate a unique node ID for a node throughout the digraph. + #-- + #: (node) -> String def node_id(node) # :nodoc: "Node_#{node.object_id}" end # Inspect a location to display the start and end line and columns in bytes. + #-- + #: (Location) -> String def location_inspect(location) # :nodoc: "(#{location.start_line},#{location.start_column})-(#{location.end_line},#{location.end_column})" end @@ -176,6 +200,8 @@ module Prism # Inspect a node that has <%= flag.human %> flags to display the flags as a # comma-separated list. + #-- + #: (<%= nodes.filter_map { |node| node.name if node.flags == flag }.join(" | ") %> node) -> String def <%= flag.human %>_inspect(node) # :nodoc: flags = [] #: Array[String] <%- flag.values.each do |value| -%> diff --git a/prism/templates/lib/prism/dsl.rb.erb b/prism/templates/lib/prism/dsl.rb.erb index e16ebb71101921..de265a095b07c5 100644 --- a/prism/templates/lib/prism/dsl.rb.erb +++ b/prism/templates/lib/prism/dsl.rb.erb @@ -1,3 +1,5 @@ +# rbs_inline: enabled + module Prism # The DSL module provides a set of methods that can be used to create prism # nodes in a more concise manner. For example, instead of writing: @@ -56,17 +58,31 @@ module Prism extend self # Create a new Source object. + #-- + #: (String string) -> Source def source(string) Source.for(string) end # Create a new Location object. + #-- + #: (?source: Source, ?start_offset: Integer, ?length: Integer) -> Location def location(source: default_source, start_offset: 0, length: 0) Location.new(source, start_offset, length) end <%- nodes.each do |node| -%> + <%- + params = [ + ["source", "Source"], + ["node_id", "Integer"], + ["location", "Location"], + ["flags", "Integer"] + ].concat(node.fields.map { |field| [field.name, field.rbs_class] }) + -%> # Create a new <%= node.name %> node. + #-- + #: (<%= params.map { |(name, type)| "?#{name}: #{type}" }.join(", ") %>) -> <%= node.name %> def <%= node.human %>(<%= ["source: default_source", "node_id: 0", "location: default_location", "flags: 0", *node.fields.map { |field| case field when Prism::Template::NodeField @@ -100,6 +116,8 @@ module Prism <%- flags.each do |flag| -%> # Retrieve the value of one of the <%= flag.name %> flags. + #-- + #: (Symbol name) -> Integer def <%= flag.human.chomp("s") %>(name) case name <%- flag.values.each do |value| -%> @@ -114,18 +132,24 @@ module Prism # The default source object that gets attached to nodes and locations if no # source is specified. + #-- + #: () -> Source def default_source Source.for("") end # The default location object that gets attached to nodes if no location is # specified, which uses the given source. + #-- + #: () -> Location def default_location Location.new(default_source, 0, 0) end # The default node that gets attached to nodes if no node is specified for a # required node field. + #-- + #: (Source source, Location location) -> node def default_node(source, location) MissingNode.new(source, -1, location, 0) end diff --git a/prism/templates/lib/prism/inspect_visitor.rb.erb b/prism/templates/lib/prism/inspect_visitor.rb.erb index 9a33cb8110f076..9b15cb3d8abc60 100644 --- a/prism/templates/lib/prism/inspect_visitor.rb.erb +++ b/prism/templates/lib/prism/inspect_visitor.rb.erb @@ -1,3 +1,5 @@ +# rbs_inline: enabled + module Prism # This visitor is responsible for composing the strings that get returned by # the various #inspect methods defined on each of the nodes. @@ -7,8 +9,9 @@ module Prism # when we hit an element in that list. In this case, we have a special # command that replaces the subsequent indent with the given value. class Replace # :nodoc: - attr_reader :value + attr_reader :value #: String + #: (String value) -> void def initialize(value) @value = value end @@ -17,18 +20,25 @@ module Prism private_constant :Replace # The current prefix string. - attr_reader :indent # :nodoc: + # :stopdoc: + attr_reader :indent #: String + # :startdoc: # The list of commands that we need to execute in order to compose the # final string. - attr_reader :commands # :nodoc: + #: stopdoc: + attr_reader :commands #: Array[[String | node | Replace, String]] + # :startdoc: + #: (?String indent) -> void def initialize(indent = +"") # :nodoc: @indent = indent @commands = [] end # Compose an inspect string for the given node. + #-- + #: (node node) -> String def self.compose(node) visitor = new node.accept(visitor) @@ -36,6 +46,8 @@ module Prism end # Compose the final string. + #-- + #: () -> String def compose # :nodoc: buffer = +"" replace = nil @@ -65,6 +77,7 @@ module Prism end <%- nodes.each do |node| -%> + #: (<%= node.name %> node) -> void def visit_<%= node.human %>(node) # :nodoc: commands << [inspect_node(<%= node.name.inspect %>, node), indent] <%- (fields = [node.flags || Prism::Template::Flags.empty, *node.fields]).each_with_index do |field, index| -%> @@ -112,12 +125,16 @@ module Prism private # Compose a header for the given node. + #-- + #: (String name, node node) -> String def inspect_node(name, node) # :nodoc: location = node.location "@ #{name} (location: (#{location.start_line},#{location.start_column})-(#{location.end_line},#{location.end_column}))\n" end # Compose a string representing the given inner location field. + #-- + #: (Location? location) -> String def inspect_location(location) # :nodoc: if location "(#{location.start_line},#{location.start_column})-(#{location.end_line},#{location.end_column}) = #{location.slice.inspect}" diff --git a/prism/templates/lib/prism/mutation_compiler.rb.erb b/prism/templates/lib/prism/mutation_compiler.rb.erb index b223860f2f6ff2..6fcbb25dcc90df 100644 --- a/prism/templates/lib/prism/mutation_compiler.rb.erb +++ b/prism/templates/lib/prism/mutation_compiler.rb.erb @@ -1,3 +1,5 @@ +# rbs_inline: enabled + module Prism # This visitor walks through the tree and copies each node as it is being # visited. This is useful for consumers that want to mutate the tree, as you @@ -5,6 +7,7 @@ module Prism class MutationCompiler < Compiler <%- nodes.each_with_index do |node, index| -%> <%= "\n" if index != 0 -%> + #: (<%= node.name %>) -> node? def visit_<%= node.human %>(node) # :nodoc: <%- fields = node.fields.select { |field| [Prism::Template::NodeField, Prism::Template::OptionalNodeField, Prism::Template::NodeListField].include?(field.class) } -%> <%- if fields.any? -%> diff --git a/prism/templates/lib/prism/node.rb.erb b/prism/templates/lib/prism/node.rb.erb index 8c88529c664f3c..195200e4824d6d 100644 --- a/prism/templates/lib/prism/node.rb.erb +++ b/prism/templates/lib/prism/node.rb.erb @@ -1,26 +1,48 @@ -# :markup: markdown +# rbs_inline: enabled module Prism + # @rbs! + # interface _Repository + # def enter: (Integer node_id, Symbol field_name) -> Relocation::Entry + # end + # + # interface _Node + # def deconstruct: () -> Array[Prism::node?] + # def inspect: () -> String + # end + # + # type node = Node & _Node + # This represents a node in the tree. It is the parent class of all of the # various node types. class Node # A pointer to the source that this node was created from. - attr_reader :source # :nodoc: + # :stopdoc: + attr_reader :source #: Source private :source + # :startdoc: # A unique identifier for this node. This is used in a very specific # use case where you want to keep around a reference to a node without # having to keep around the syntax tree in memory. This unique identifier # will be consistent across multiple parses of the same source code. - attr_reader :node_id + attr_reader :node_id #: Integer + + # The location associated with this node. For lazily loading Location + # objects, we keep it as a packed integer until it is accessed. + # @rbs @location: Location | Integer # Save this node using a saved source so that it can be retrieved later. + #-- + #: (_Repository repository) -> Relocation::Entry def save(repository) repository.enter(node_id, :itself) end # A Location instance that represents the location of this node in the # source. + #-- + #: () -> Location def location location = @location return location if location.is_a?(Location) @@ -28,6 +50,8 @@ module Prism end # Save the location using a saved source so that it can be retrieved later. + #-- + #: (_Repository repository) -> Relocation::Entry def save_location(repository) repository.enter(node_id, :location) end @@ -38,22 +62,30 @@ module Prism # -------------------------------------------------------------------------- # Delegates to [`start_line`](rdoc-ref:Location#start_line) of the associated location object. + #-- + #: () -> Integer def start_line location.start_line end # Delegates to [`end_line`](rdoc-ref:Location#end_line) of the associated location object. + #-- + #: () -> Integer def end_line location.end_line end # Delegates to [`start_offset`](rdoc-ref:Location#start_offset) of the associated location object. + #-- + #: () -> Integer def start_offset location = @location location.is_a?(Location) ? location.start_offset : location >> 32 end # Delegates to [`end_offset`](rdoc-ref:Location#end_offset) of the associated location object. + #-- + #: () -> Integer def end_offset location = @location location.is_a?(Location) ? location.end_offset : ((location >> 32) + (location & 0xFFFFFFFF)) @@ -61,73 +93,99 @@ module Prism # Delegates to [`start_character_offset`](rdoc-ref:Location#start_character_offset) # of the associated location object. + #-- + #: () -> Integer def start_character_offset location.start_character_offset end # Delegates to [`end_character_offset`](rdoc-ref:Location#end_character_offset) # of the associated location object. + #-- + #: () -> Integer def end_character_offset location.end_character_offset end # Delegates to [`cached_start_code_units_offset`](rdoc-ref:Location#cached_start_code_units_offset) # of the associated location object. + #-- + #: (_CodeUnitsCache cache) -> Integer def cached_start_code_units_offset(cache) location.cached_start_code_units_offset(cache) end # Delegates to [`cached_end_code_units_offset`](rdoc-ref:Location#cached_end_code_units_offset) # of the associated location object. + #-- + #: (_CodeUnitsCache cache) -> Integer def cached_end_code_units_offset(cache) location.cached_end_code_units_offset(cache) end # Delegates to [`start_column`](rdoc-ref:Location#start_column) of the associated location object. + #-- + #: () -> Integer def start_column location.start_column end # Delegates to [`end_column`](rdoc-ref:Location#end_column) of the associated location object. + #-- + #: () -> Integer def end_column location.end_column end # Delegates to [`start_character_column`](rdoc-ref:Location#start_character_column) # of the associated location object. + #-- + #: () -> Integer def start_character_column location.start_character_column end # Delegates to [`end_character_column`](rdoc-ref:Location#end_character_column) # of the associated location object. + #-- + #: () -> Integer def end_character_column location.end_character_column end # Delegates to [`cached_start_code_units_column`](rdoc-ref:Location#cached_start_code_units_column) # of the associated location object. + #-- + #: (_CodeUnitsCache cache) -> Integer def cached_start_code_units_column(cache) location.cached_start_code_units_column(cache) end # Delegates to [`cached_end_code_units_column`](rdoc-ref:Location#cached_end_code_units_column) # of the associated location object. + #-- + #: (_CodeUnitsCache cache) -> Integer def cached_end_code_units_column(cache) location.cached_end_code_units_column(cache) end # Delegates to [`leading_comments`](rdoc-ref:Location#leading_comments) of the associated location object. + #-- + #: () -> Array[Comment] def leading_comments location.leading_comments end # Delegates to [`trailing_comments`](rdoc-ref:Location#trailing_comments) of the associated location object. + #-- + #: () -> Array[Comment] def trailing_comments location.trailing_comments end # Delegates to [`comments`](rdoc-ref:Location#comments) of the associated location object. + #-- + #: () -> Array[Comment] def comments location.comments end @@ -135,6 +193,8 @@ module Prism # :section: # Returns all of the lines of the source code associated with this node. + #-- + #: () -> Array[String] def source_lines location.source_lines end @@ -144,6 +204,8 @@ module Prism alias script_lines source_lines # Slice the location of the node from the source. + #-- + #: () -> String def slice location.slice end @@ -151,27 +213,37 @@ module Prism # Slice the location of the node from the source, starting at the beginning # of the line that the location starts on, ending at the end of the line # that the location ends on. + #-- + #: () -> String def slice_lines location.slice_lines end # An bitset of flags for this node. There are certain flags that are common # for all nodes, and then some nodes have specific flags. - attr_reader :flags # :nodoc: + # :stopdoc: + attr_reader :flags #: Integer protected :flags + # :startdoc: # Returns true if the node has the newline flag set. + #-- + #: () -> bool def newline? flags.anybits?(NodeFlags::NEWLINE) end # Returns true if the node has the static literal flag set. + #-- + #: () -> bool def static_literal? flags.anybits?(NodeFlags::STATIC_LITERAL) end # Similar to inspect, but respects the current level of indentation given by # the pretty print object. + #-- + #: (PP q) -> void def pretty_print(q) # :nodoc: q.seplist(inspect.chomp.each_line, -> { q.breakable }) do |line| q.text(line.chomp) @@ -180,6 +252,8 @@ module Prism end # Convert this node into a graphviz dot graph string. + #-- + #: () -> String def to_dot # @type self: node DotVisitor.new.tap { |visitor| accept(visitor) }.to_dot @@ -191,9 +265,11 @@ module Prism # # Important to note is that the column given to this method should be in # bytes, as opposed to characters or code units. + #-- + #: (Integer line, Integer column) -> Array[node] def tunnel(line, column) - queue = [self] #: Array[Prism::node] - result = [] #: Array[Prism::node] + queue = [self] #: Array[node] + result = [] #: Array[node] offset = source.byte_offset(line, column) while (node = queue.shift) @@ -215,9 +291,10 @@ module Prism # particular condition. # # node.breadth_first_search { |node| node.node_id == node_id } - # + #-- + #: () { (node) -> bool } -> node? def breadth_first_search(&block) - queue = [self] #: Array[Prism::node] + queue = [self] #: Array[node] while (node = queue.shift) return node if yield node @@ -233,7 +310,8 @@ module Prism # particular condition. # # node.breadth_first_search_all { |node| node.is_a?(Prism::CallNode) } - # + #-- + #: () { (node) -> bool } -> Array[node] def breadth_first_search_all(&block) queue = [self] #: Array[Prism::node] results = [] #: Array[Prism::node] @@ -250,6 +328,8 @@ module Prism # Returns a list of the fields that exist for this node class. Fields # describe the structure of the node. This kind of reflection is useful for # things like recursively visiting each node _and_ field in the tree. + #-- + #: () -> Array[Reflection::Field] def self.fields # This method should only be called on subclasses of Node, not Node # itself. @@ -265,12 +345,16 @@ module Prism # -------------------------------------------------------------------------- # Accepts a visitor and calls back into the specialized visit function. + #-- + #: (_Visitor visitor) -> untyped def accept(visitor) raise NoMethodError, "undefined method `accept' for #{inspect}" end # Returns an array of child nodes, including `nil`s in the place of optional # nodes that were not present. + #-- + #: () -> Array[node?] def child_nodes raise NoMethodError, "undefined method `child_nodes' for #{inspect}" end @@ -280,23 +364,32 @@ module Prism # With a block given, yields each child node. Without a block, returns # an enumerator that contains each child node. Excludes any `nil`s in # the place of optional nodes that were not present. + #-- + #: () { (node) -> void } -> void + #: () -> Enumerator[node, void] def each_child_node raise NoMethodError, "undefined method `each_child_node' for #{inspect}" end # Returns an array of child nodes, excluding any `nil`s in the place of # optional nodes that were not present. + #-- + #: () -> Array[node] def compact_child_nodes raise NoMethodError, "undefined method `compact_child_nodes' for #{inspect}" end # Returns an array of child nodes and locations that could potentially have # comments attached to them. + #-- + #: () -> Array[node | Location] def comment_targets raise NoMethodError, "undefined method `comment_targets' for #{inspect}" end # Returns a string representation of the node. + #-- + #: () -> String def inspect raise NoMethodError, "undefined method `inspect' for #{inspect}" end @@ -313,6 +406,8 @@ module Prism # it uses a single integer comparison, but also because if you're on CRuby # you can take advantage of the fact that case statements with all symbol # keys will use a jump table. + #-- + #: () -> Symbol def type raise NoMethodError, "undefined method `type' for #{inspect}" end @@ -321,6 +416,8 @@ module Prism # splitting on the type of the node without having to do a long === chain. # Note that like #type, it will still be slower than using == for a single # class, but should be faster in a case statement or an array comparison. + #-- + #: () -> Symbol def self.type raise NoMethodError, "undefined method `type' for #{inspect}" end @@ -331,7 +428,13 @@ module Prism #<%= line %> <%- end -%> class <%= node.name -%> < Node + <%- node.fields.each do |field| -%> + # @rbs @<%= field.name %>: <%= field.rbs_class %> + <%- end -%> + # Initialize a new <%= node.name %> node. + #-- + #: (Source source, Integer node_id, Location location, Integer flags, <%= node.fields.map { |field| "#{field.rbs_class} #{field.name}" }.join(", ") %>) -> void def initialize(<%= ["source", "node_id", "location", "flags", *node.fields.map(&:name)].join(", ") %>) @source = source @node_id = node_id @@ -357,11 +460,15 @@ module Prism # ---------------------------------------------------------------------------------- # See Node.accept. + #-- + #: (_Visitor visitor) -> untyped def accept(visitor) visitor.visit_<%= node.human %>(self) end # See Node.child_nodes. + #-- + #: () -> Array[node?] def child_nodes [<%= node.fields.map { |field| case field @@ -372,6 +479,9 @@ module Prism end # See Node.each_child_node. + #-- + #: () { (node) -> void } -> void + #: () -> Enumerator[node, void] def each_child_node return to_enum(:each_child_node) unless block_given? @@ -380,7 +490,7 @@ module Prism <%- when Prism::Template::NodeField -%> yield <%= field.name %> <%- when Prism::Template::OptionalNodeField -%> - yield <%= field.name %> if <%= field.name %> + if (<%= field.name %> = self.<%= field.name %>); yield <%= field.name %>; end <%- when Prism::Template::NodeListField -%> <%= field.name %>.each { |node| yield node } <%- end -%> @@ -388,6 +498,8 @@ module Prism end # See Node.compact_child_nodes. + #-- + #: () -> Array[node] def compact_child_nodes <%- if node.fields.any? { |field| field.is_a?(Prism::Template::OptionalNodeField) } -%> compact = [] #: Array[Prism::node] @@ -396,7 +508,7 @@ module Prism <%- when Prism::Template::NodeField -%> compact << <%= field.name %> <%- when Prism::Template::OptionalNodeField -%> - compact << <%= field.name %> if <%= field.name %> + if (<%= field.name %> = self.<%= field.name %>); compact << <%= field.name %>; end <%- when Prism::Template::NodeListField -%> compact.concat(<%= field.name %>) <%- end -%> @@ -413,6 +525,8 @@ module Prism end # See Node.comment_targets. + #-- + #: () -> Array[node | Location] def comment_targets [<%= node.fields.map { |field| case field @@ -426,26 +540,34 @@ module Prism # copy(**fields) -> <%= node.name %> # # Creates a copy of self with the given fields, using self as the template. + #-- + #: (?node_id: Integer, ?location: Location, ?flags: Integer, <%= node.fields.map { |field| "?#{field.name}: #{field.rbs_class}" }.join(", ") %>) -> <%= node.name %> def copy(<%= (["node_id", "location", "flags"] + node.fields.map(&:name)).map { |field| "#{field}: self.#{field}" }.join(", ") %>) <%= node.name %>.new(<%= ["source", "node_id", "location", "flags", *node.fields.map(&:name)].join(", ") %>) end alias deconstruct child_nodes + #: (Array[Symbol]? keys) -> Hash[Symbol, untyped] def deconstruct_keys(keys) # :nodoc: { <%= (["node_id: node_id", "location: location"] + node.fields.map { |field| "#{field.name}: #{field.name}" }).join(", ") %> } end # See `Node#type`. + #-- + #: () -> :<%= node.human %> def type :<%= node.human %> end # See `Node.type`. + #-- + #: () -> :<%= node.human %> def self.type :<%= node.human %> end + #: () -> String def inspect # :nodoc: InspectVisitor.compose(self) end @@ -456,6 +578,8 @@ module Prism <%- node_flags.values.each do |value| -%> # :category: Flags # <%= value.comment %> + #-- + #: () -> bool def <%= value.name.downcase %>? flags.anybits?(<%= node_flags.name %>::<%= value.name %>) end @@ -476,6 +600,8 @@ module Prism #<%= line %> <%- end -%> <%- end -%> + #-- + #: () -> Location def <%= field.name %> location = @<%= field.name %> return location if location.is_a?(Location) @@ -485,6 +611,8 @@ module Prism # :category: Repository # Save the <%= field.name %> location using the given saved source so that # it can be retrieved later. + #-- + #: (_Repository repository) -> Relocation::Entry def save_<%= field.name %>(repository) repository.enter(node_id, :<%= field.name %>) end @@ -501,6 +629,8 @@ module Prism #<%= line %> <%- end -%> <%- end -%> + #-- + #: () -> Location? def <%= field.name %> location = @<%= field.name %> case location @@ -516,6 +646,8 @@ module Prism # :category: Repository # Save the <%= field.name %> location using the given saved source so that # it can be retrieved later. + #-- + #: (_Repository repository) -> Relocation::Entry? def save_<%= field.name %>(repository) repository.enter(node_id, :<%= field.name %>) unless @<%= field.name %>.nil? end @@ -530,6 +662,8 @@ module Prism #<%= line %> <%- end -%> <%- end -%> + #-- + #: () -> <%= field.rbs_class %> def <%= field.name %> @<%= field.name %> end @@ -547,6 +681,8 @@ module Prism # <%= field.name.delete_suffix("_loc") %> -> String # # Slice the location of <%= field.name %> from the source. + #-- + #: () -> String def <%= field.name.delete_suffix("_loc") %> <%= field.name %>.slice end @@ -558,6 +694,8 @@ module Prism # <%= field.name.delete_suffix("_loc") %> -> String | nil # # Slice the location of <%= field.name %> from the source. + #-- + #: () -> String? def <%= field.name.delete_suffix("_loc") %> <%= field.name %>&.slice end @@ -566,6 +704,7 @@ module Prism <%- end -%> # :section: + #: (untyped other) -> bool def ===(other) # :nodoc: other.is_a?(<%= node.name %>)<%= " &&" if (fields = [*node.flags, *node.fields]).any? %> <%- fields.each_with_index do |field, index| -%> diff --git a/prism/templates/lib/prism/reflection.rb.erb b/prism/templates/lib/prism/reflection.rb.erb index 6c8b2f4d257714..d5ace99259ba96 100644 --- a/prism/templates/lib/prism/reflection.rb.erb +++ b/prism/templates/lib/prism/reflection.rb.erb @@ -1,3 +1,5 @@ +# rbs_inline: enabled + module Prism # The Reflection module provides the ability to reflect on the structure of # the syntax tree itself, as opposed to looking at a single syntax tree. This @@ -7,9 +9,11 @@ module Prism # for all other field types. class Field # The name of the field. - attr_reader :name + attr_reader :name #: Symbol # Initializes the field with the given name. + #-- + #: (Symbol name) -> void def initialize(name) @name = name end @@ -83,9 +87,11 @@ module Prism # the bitset should be accessed through their query methods. class FlagsField < Field # The names of the flags in the bitset. - attr_reader :flags + attr_reader :flags #: Array[Symbol] # Initializes the flags field with the given name and flags. + #-- + #: (Symbol name, Array[Symbol] flags) -> void def initialize(name, flags) super(name) @flags = flags @@ -93,6 +99,8 @@ module Prism end # Returns the fields for the given node. + #-- + #: (singleton(Node) node) -> Array[Field] def self.fields_for(node) case node.type <%- nodes.each do |node| -%> diff --git a/prism/templates/lib/prism/serialize.rb.erb b/prism/templates/lib/prism/serialize.rb.erb index 63ef07cb6e9dbd..229322291e42c6 100644 --- a/prism/templates/lib/prism/serialize.rb.erb +++ b/prism/templates/lib/prism/serialize.rb.erb @@ -1,3 +1,5 @@ +# rbs_inline: enabled + require "stringio" require_relative "polyfill/unpack1" @@ -20,6 +22,8 @@ module Prism # # The formatting of the source of this method is purposeful to illustrate # the structure of the serialized data. + #-- + #: (String input, String serialized, bool freeze) -> ParseResult def self.load_parse(input, serialized, freeze) input = input.dup source = Source.for(input) @@ -43,7 +47,7 @@ module Prism constant_pool = ConstantPool.new(input, serialized, cpool_base, cpool_size) - node = loader.load_node(constant_pool, encoding, freeze) + node = loader.load_node(constant_pool, encoding, freeze) #: ProgramNode loader.load_constant_pool(constant_pool) raise unless loader.eof? @@ -73,6 +77,8 @@ module Prism # # The formatting of the source of this method is purposeful to illustrate # the structure of the serialized data. + #-- + #: (String input, String serialized, bool freeze) -> LexResult def self.load_lex(input, serialized, freeze) source = Source.for(input) loader = Loader.new(source, serialized) @@ -117,6 +123,8 @@ module Prism # # The formatting of the source of this method is purposeful to illustrate # the structure of the serialized data. + #-- + #: (String input, String serialized, bool freeze) -> Array[Comment] def self.load_parse_comments(input, serialized, freeze) source = Source.for(input) loader = Loader.new(source, serialized) @@ -139,6 +147,8 @@ module Prism # # The formatting of the source of this method is purposeful to illustrate # the structure of the serialized data. + #-- + #: (String input, String serialized, bool freeze) -> ParseLexResult def self.load_parse_lex(input, serialized, freeze) source = Source.for(input) loader = Loader.new(source, serialized) @@ -162,11 +172,11 @@ module Prism constant_pool = ConstantPool.new(input, serialized, cpool_base, cpool_size) - node = loader.load_node(constant_pool, encoding, freeze) + node = loader.load_node(constant_pool, encoding, freeze) #: ProgramNode loader.load_constant_pool(constant_pool) raise unless loader.eof? - value = [node, tokens] + value = [node, tokens] #: [ProgramNode, Array[[Token, Integer]]] result = ParseLexResult.new(value, comments, magic_comments, data_loc, errors, warnings, source) tokens.each do |token| @@ -189,8 +199,14 @@ module Prism end class ConstantPool # :nodoc: - attr_reader :size + attr_reader :size #: Integer + + # @rbs @input: String + # @rbs @serialized: String + # @rbs @base: Integer + # @rbs @pool: Array[Symbol?] + #: (String input, String serialized, Integer base, Integer size) -> void def initialize(input, serialized, base, size) @input = input @serialized = serialized @@ -199,17 +215,18 @@ module Prism @pool = Array.new(size, nil) end + #: (Integer index, Encoding encoding) -> Symbol def get(index, encoding) @pool[index] ||= begin offset = @base + index * 8 - start = @serialized.unpack1("L", offset: offset) - length = @serialized.unpack1("L", offset: offset + 4) + start = @serialized.unpack1("L", offset: offset) #: Integer + length = @serialized.unpack1("L", offset: offset + 4) #: Integer if start.nobits?(1 << 31) - @input.byteslice(start, length).force_encoding(encoding).to_sym + (@input.byteslice(start, length) or raise).force_encoding(encoding).to_sym else - @serialized.byteslice(start & ((1 << 31) - 1), length).force_encoding(encoding).to_sym + (@serialized.byteslice(start & ((1 << 31) - 1), length) or raise).force_encoding(encoding).to_sym end end end @@ -217,6 +234,7 @@ module Prism if RUBY_ENGINE == "truffleruby" # StringIO is synchronized and that adds a high overhead on TruffleRuby. + # @rbs skip class FastStringIO # :nodoc: attr_accessor :pos @@ -246,8 +264,11 @@ module Prism end class Loader # :nodoc: - attr_reader :input, :io, :source + attr_reader :input #: String + attr_reader :io #: StringIO + attr_reader :source #: Source + #: (Source source, String serialized) -> void def initialize(source, serialized) @input = source.source.dup raise unless serialized.encoding == Encoding::BINARY @@ -256,40 +277,46 @@ module Prism define_load_node_lambdas if RUBY_ENGINE != "ruby" end + #: () -> bool def eof? io.getbyte io.eof? end + #: (ConstantPool constant_pool) -> void def load_constant_pool(constant_pool) trailer = 0 constant_pool.size.times do |index| - start, length = io.read(8).unpack("L2") + start, length = (io.read(8) or raise).unpack("L2") #: [Integer, Integer] trailer += length if start.anybits?(1 << 31) end io.read(trailer) end + #: () -> void def load_header raise "Invalid serialization" if io.read(5) != "PRISM" - raise "Invalid serialization" if io.read(3).unpack("C3") != [MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION] + raise "Invalid serialization" if (io.read(3) or raise).unpack("C3") != [MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION] raise "Invalid serialization (location fields must be included but are not)" if io.getbyte != 0 end + #: () -> Encoding def load_encoding - encoding = Encoding.find(io.read(load_varuint)) + encoding = Encoding.find((io.read(load_varuint) or raise)) or raise @input = input.force_encoding(encoding).freeze encoding end + #: (bool freeze) -> Array[Integer] def load_line_offsets(freeze) offsets = Array.new(load_varuint) { load_varuint } offsets.freeze if freeze offsets end + #: (bool freeze) -> Array[Comment] def load_comments(freeze) comments = Array.new(load_varuint) do @@ -297,6 +324,7 @@ module Prism case load_varuint when 0 then InlineComment.new(load_location_object(freeze)) when 1 then EmbDocComment.new(load_location_object(freeze)) + else raise end comment.freeze if freeze @@ -307,6 +335,7 @@ module Prism comments end + #: (bool freeze) -> Array[MagicComment] def load_magic_comments(freeze) magic_comments = Array.new(load_varuint) do @@ -331,10 +360,11 @@ module Prism <%- warnings.each do |warning| -%> <%= warning.name.downcase.to_sym.inspect %>, <%- end -%> - ].freeze + ].freeze #: Array[Symbol] private_constant :DIAGNOSTIC_TYPES + #: () -> Symbol def load_error_level level = io.getbyte @@ -350,6 +380,7 @@ module Prism end end + #: (Encoding encoding, bool freeze) -> Array[ParseError] def load_errors(encoding, freeze) errors = Array.new(load_varuint) do @@ -369,6 +400,7 @@ module Prism errors end + #: () -> Symbol def load_warning_level level = io.getbyte @@ -382,6 +414,7 @@ module Prism end end + #: (Encoding encoding, bool freeze) -> Array[ParseWarning] def load_warnings(encoding, freeze) warnings = Array.new(load_varuint) do @@ -401,8 +434,9 @@ module Prism warnings end + #: () -> Array[[Token, Integer]] def load_tokens - tokens = [] + tokens = [] #: Array[[Token, Integer]] while (type = TOKEN_TYPES.fetch(load_varuint)) start = load_varuint @@ -420,25 +454,29 @@ module Prism # variable-length integer using https://en.wikipedia.org/wiki/LEB128 # This is also what protobuf uses: https://protobuf.dev/programming-guides/encoding/#varints + #-- + #: () -> Integer def load_varuint - n = io.getbyte + n = (io.getbyte or raise) if n < 128 n else n -= 128 shift = 0 - while (b = io.getbyte) >= 128 + while (b = (io.getbyte or raise)) >= 128 n += (b - 128) << (shift += 7) end n + (b << (shift + 7)) end end + #: () -> Integer def load_varsint n = load_varuint (n >> 1) ^ (-(n & 1)) end + #: () -> Integer def load_integer negative = io.getbyte != 0 length = load_varuint @@ -450,14 +488,17 @@ module Prism value end + #: () -> Float def load_double - io.read(8).unpack1("D") + (io.read(8) or raise).unpack1("D") #: Float end + #: () -> Integer def load_uint32 - io.read(4).unpack1("L") + (io.read(4) or raise).unpack1("L") #: Integer end + #: (ConstantPool constant_pool, Encoding encoding, bool freeze) -> node? def load_optional_node(constant_pool, encoding, freeze) if io.getbyte != 0 io.pos -= 1 @@ -465,14 +506,16 @@ module Prism end end + #: (Encoding encoding) -> String def load_embedded_string(encoding) - io.read(load_varuint).force_encoding(encoding).freeze + (io.read(load_varuint) or raise).force_encoding(encoding).freeze end + #: (Encoding encoding) -> String def load_string(encoding) case (type = io.getbyte) when 1 - input.byteslice(load_varuint, load_varuint).force_encoding(encoding).freeze + (input.byteslice(load_varuint, load_varuint) or raise).force_encoding(encoding).freeze when 2 load_embedded_string(encoding) else @@ -480,75 +523,116 @@ module Prism end end + #: (bool freeze) -> Location def load_location_object(freeze) location = Location.new(source, load_varuint, load_varuint) location.freeze if freeze location end + # Load a location object from the serialized data. Note that we are lying + # about the signature a bit here, because we sometimes load it as a packed + # integer instead of an object. + #-- + #: (bool freeze) -> Location def load_location(freeze) return load_location_object(freeze) if freeze - (load_varuint << 32) | load_varuint + (load_varuint << 32) | load_varuint #: Location end + # Load an optional location object from the serialized data if it is + # present. Note that we are lying about the signature a bit here, because + # we sometimes load it as a packed integer instead of an object. + #-- + #: (bool freeze) -> Location? def load_optional_location(freeze) load_location(freeze) if io.getbyte != 0 end + #: (bool freeze) -> Location? def load_optional_location_object(freeze) load_location_object(freeze) if io.getbyte != 0 end + #: (ConstantPool constant_pool, Encoding encoding) -> Symbol def load_constant(constant_pool, encoding) index = load_varuint constant_pool.get(index - 1, encoding) end + #: (ConstantPool constant_pool, Encoding encoding) -> Symbol? def load_optional_constant(constant_pool, encoding) index = load_varuint constant_pool.get(index - 1, encoding) if index != 0 end if RUBY_ENGINE == "ruby" + #: (ConstantPool constant_pool, Encoding encoding, bool freeze) -> node def load_node(constant_pool, encoding, freeze) type = io.getbyte node_id = load_varuint - location = load_location(freeze) - value = case type - <%- nodes.each_with_index do |node, index| -%> - when <%= index + 1 %> then - <%- if node.needs_serialized_length? -%> - load_uint32 - <%- end -%> - <%= node.name %>.new(<%= ["source", "node_id", "location", "load_varuint", *node.fields.map { |field| - case field - when Prism::Template::NodeField then "load_node(constant_pool, encoding, freeze)" - when Prism::Template::OptionalNodeField then "load_optional_node(constant_pool, encoding, freeze)" - when Prism::Template::StringField then "load_string(encoding)" - when Prism::Template::NodeListField then "Array.new(load_varuint) { load_node(constant_pool, encoding, freeze) }.tap { |nodes| nodes.freeze if freeze }" - when Prism::Template::ConstantField then "load_constant(constant_pool, encoding)" - when Prism::Template::OptionalConstantField then "load_optional_constant(constant_pool, encoding)" - when Prism::Template::ConstantListField then "Array.new(load_varuint) { load_constant(constant_pool, encoding) }.tap { |constants| constants.freeze if freeze }" - when Prism::Template::LocationField then "load_location(freeze)" - when Prism::Template::OptionalLocationField then "load_optional_location(freeze)" - when Prism::Template::UInt8Field then "io.getbyte" - when Prism::Template::UInt32Field then "load_varuint" - when Prism::Template::IntegerField then "load_integer" - when Prism::Template::DoubleField then "load_double" - else raise - end - }].join(", ") -%>) + location = load_location(freeze) #: Location + value = + case type + <%- nodes.each_with_index do |node, index| -%> + when <%= index + 1 %> + <%- if node.needs_serialized_length? -%> + load_uint32 + <%- end -%> + <%= node.name %>.new( + source, + node_id, + location, + load_varuint, + <%- node.fields.each do |field| -%> + <%- case field -%> + <%- when Prism::Template::NodeField -%> + load_node(constant_pool, encoding, freeze), #: <%= field.rbs_class %> + <%- when Prism::Template::OptionalNodeField -%> + load_optional_node(constant_pool, encoding, freeze), #: <%= field.rbs_class %> + <%- when Prism::Template::StringField -%> + load_string(encoding), + <%- when Prism::Template::NodeListField -%> + Array.new(load_varuint) do + load_node(constant_pool, encoding, freeze) #: <%= field.element_rbs_class %> + end.tap { |nodes| nodes.freeze if freeze }, + <%- when Prism::Template::ConstantField -%> + load_constant(constant_pool, encoding), + <%- when Prism::Template::OptionalConstantField -%> + load_optional_constant(constant_pool, encoding), + <%- when Prism::Template::ConstantListField -%> + Array.new(load_varuint) { load_constant(constant_pool, encoding) }.tap { |constants| constants.freeze if freeze }, + <%- when Prism::Template::LocationField -%> + load_location(freeze), + <%- when Prism::Template::OptionalLocationField -%> + load_optional_location(freeze), + <%- when Prism::Template::UInt8Field -%> + (io.getbyte or raise), + <%- when Prism::Template::UInt32Field -%> + load_varuint, + <%- when Prism::Template::IntegerField -%> + load_integer, + <%- when Prism::Template::DoubleField -%> + load_double, + <%- else raise -%> + <%- end -%> + <%- end -%> + ) <%- end -%> - end + else + raise "Unknown node type: #{type}" + end value.freeze if freeze value end else + # @rbs skip def load_node(constant_pool, encoding, freeze) - @load_node_lambdas[io.getbyte].call(constant_pool, encoding, freeze) + @load_node_lambdas[(io.getbyte or raise)].call(constant_pool, encoding, freeze) end + # @rbs skip def define_load_node_lambdas @load_node_lambdas = [ nil, @@ -559,24 +643,46 @@ module Prism <%- if node.needs_serialized_length? -%> load_uint32 <%- end -%> - value = <%= node.name %>.new(<%= ["source", "node_id", "location", "load_varuint", *node.fields.map { |field| - case field - when Prism::Template::NodeField then "load_node(constant_pool, encoding, freeze)" - when Prism::Template::OptionalNodeField then "load_optional_node(constant_pool, encoding, freeze)" - when Prism::Template::StringField then "load_string(encoding)" - when Prism::Template::NodeListField then "Array.new(load_varuint) { load_node(constant_pool, encoding, freeze) }" - when Prism::Template::ConstantField then "load_constant(constant_pool, encoding)" - when Prism::Template::OptionalConstantField then "load_optional_constant(constant_pool, encoding)" - when Prism::Template::ConstantListField then "Array.new(load_varuint) { load_constant(constant_pool, encoding) }" - when Prism::Template::LocationField then "load_location(freeze)" - when Prism::Template::OptionalLocationField then "load_optional_location(freeze)" - when Prism::Template::UInt8Field then "io.getbyte" - when Prism::Template::UInt32Field then "load_varuint" - when Prism::Template::IntegerField then "load_integer" - when Prism::Template::DoubleField then "load_double" - else raise - end - }].join(", ") -%>) + value = + <%= node.name %>.new( + source, + node_id, + location, + load_varuint, + <%- node.fields.map do |field| -%> + <%- case field -%> + <%- when Prism::Template::NodeField -%> + load_node(constant_pool, encoding, freeze), #: <%= field.rbs_class %> + <%- when Prism::Template::OptionalNodeField -%> + load_optional_node(constant_pool, encoding, freeze), #: <%= field.rbs_class %> + <%- when Prism::Template::StringField -%> + load_string(encoding), + <%- when Prism::Template::NodeListField -%> + Array.new(load_varuint) do + load_node(constant_pool, encoding, freeze) #: <%= field.element_rbs_class %> + end, + <%- when Prism::Template::ConstantField -%> + load_constant(constant_pool, encoding), + <%- when Prism::Template::OptionalConstantField -%> + load_optional_constant(constant_pool, encoding), + <%- when Prism::Template::ConstantListField -%> + Array.new(load_varuint) { load_constant(constant_pool, encoding) }, + <%- when Prism::Template::LocationField -%> + load_location(freeze), + <%- when Prism::Template::OptionalLocationField -%> + load_optional_location(freeze), + <%- when Prism::Template::UInt8Field -%> + (io.getbyte or raise), + <%- when Prism::Template::UInt32Field -%> + load_varuint, + <%- when Prism::Template::IntegerField -%> + load_integer, + <%- when Prism::Template::DoubleField -%> + load_double, + <%- else raise -%> + <%- end -%> + <%- end -%> + ) value.freeze if freeze value }, @@ -584,6 +690,10 @@ module Prism ] end end + + # @rbs! + # @load_node_lambdas: Array[Proc] + # def define_load_node_lambdas: () -> void end # The token types that can be indexed by their enum values. @@ -592,7 +702,7 @@ module Prism <%- tokens.each do |token| -%> <%= token.name.to_sym.inspect %>, <%- end -%> - ].freeze + ].freeze #: Array[Symbol?] private_constant :MAJOR_VERSION, :MINOR_VERSION, :PATCH_VERSION private_constant :ConstantPool, :FastStringIO, :Loader, :TOKEN_TYPES diff --git a/prism/templates/lib/prism/visitor.rb.erb b/prism/templates/lib/prism/visitor.rb.erb index 76f907724fd5da..78726c53f1324e 100644 --- a/prism/templates/lib/prism/visitor.rb.erb +++ b/prism/templates/lib/prism/visitor.rb.erb @@ -1,4 +1,13 @@ +# rbs_inline: enabled + module Prism + # @rbs! + # interface _Visitor + # <% nodes.each do |node| %> + # def visit_<%= node.human %>: (<%= node.name %>) -> void + # <% end %> + # end + # A class that knows how to walk down the tree. None of the individual visit # methods are implemented on this visitor, so it forces the consumer to # implement each one that they need. For a default implementation that @@ -6,18 +15,24 @@ module Prism class BasicVisitor # Calls `accept` on the given node if it is not `nil`, which in turn should # call back into this visitor by calling the appropriate `visit_*` method. + #-- + #: (node? node) -> void def visit(node) # @type self: _Visitor node&.accept(self) end # Visits each node in `nodes` by calling `accept` on each one. + #-- + #: (Array[node?] nodes) -> void def visit_all(nodes) # @type self: _Visitor nodes.each { |node| node&.accept(self) } end # Visits the child nodes of `node` by calling `accept` on each one. + #-- + #: (node node) -> void def visit_child_nodes(node) # @type self: _Visitor node.each_child_node { |node| node.accept(self) } @@ -47,6 +62,8 @@ module Prism <%- nodes.each_with_index do |node, index| -%> <%= "\n" if index != 0 -%> # Visit a <%= node.name %> node + #-- + #: (<%= node.name %> node) -> void def visit_<%= node.human %>(node) node.each_child_node { |node| node.accept(self) } end diff --git a/prism/templates/template.rb b/prism/templates/template.rb index 18da0647a007bd..a39f8bbb784297 100755 --- a/prism/templates/template.rb +++ b/prism/templates/template.rb @@ -150,7 +150,7 @@ def rbs_class if specific_kind specific_kind elsif union_kind - union_kind.join(" | ") + "(#{union_kind.join(" | ")})" else "Prism::node" end @@ -192,7 +192,7 @@ def rbs_class if specific_kind "#{specific_kind}?" elsif union_kind - [*union_kind, "nil"].join(" | ") + "(#{union_kind.join(" | ")})?" else "Prism::node?" end @@ -230,16 +230,20 @@ def check_field_kind # This represents a field on a node that is a list of nodes. We pass them as # references and store them directly on the struct. class NodeListField < NodeKindField - def rbs_class + def element_rbs_class if specific_kind - "Array[#{specific_kind}]" + "#{specific_kind}" elsif union_kind - "Array[#{union_kind.join(" | ")}]" + "#{union_kind.join(" | ")}" else - "Array[Prism::node]" + "Prism::node" end end + def rbs_class + "Array[#{element_rbs_class}]" + end + def call_seq_type if specific_kind "Array[#{specific_kind}]" @@ -639,13 +643,6 @@ def render(name, write_to: nil) ++ =end - HEADING - when ".rbs" - <<~HEADING - # This file is generated by the templates/template.rb script and should not be - # modified manually. See #{filepath} - # if you are looking to modify the template - HEADING when ".rbi" <<~HEADING @@ -744,13 +741,7 @@ def locals "src/token_type.c", "rbi/prism/dsl.rbi", "rbi/prism/node.rbi", - "rbi/prism/visitor.rbi", - "sig/prism.rbs", - "sig/prism/dsl.rbs", - "sig/prism/mutation_compiler.rbs", - "sig/prism/node.rbs", - "sig/prism/visitor.rbs", - "sig/prism/_private/dot_visitor.rbs" + "rbi/prism/visitor.rbi" ] end end From bc1d75adced050f01248a96ba90ded345ffc08db Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:05:31 +0100 Subject: [PATCH 2/8] [ruby/prism] Don't document rbs inline magic comment https://github.com/ruby/prism/commit/e371985c91 --- lib/prism.rb | 3 ++- lib/prism/desugar_compiler.rb | 3 ++- lib/prism/lex_compat.rb | 3 ++- lib/prism/node_ext.rb | 3 ++- lib/prism/parse_result.rb | 3 ++- lib/prism/parse_result/comments.rb | 3 ++- lib/prism/parse_result/errors.rb | 3 ++- lib/prism/parse_result/newlines.rb | 3 ++- lib/prism/pattern.rb | 3 ++- lib/prism/relocation.rb | 3 ++- lib/prism/string_query.rb | 3 ++- lib/prism/translation.rb | 3 ++- lib/prism/translation/parser_current.rb | 1 + prism/templates/lib/prism/compiler.rb.erb | 1 + prism/templates/lib/prism/dispatcher.rb.erb | 1 + prism/templates/lib/prism/dot_visitor.rb.erb | 1 + prism/templates/lib/prism/dsl.rb.erb | 1 + prism/templates/lib/prism/inspect_visitor.rb.erb | 1 + prism/templates/lib/prism/mutation_compiler.rb.erb | 1 + prism/templates/lib/prism/node.rb.erb | 1 + prism/templates/lib/prism/reflection.rb.erb | 1 + prism/templates/lib/prism/serialize.rb.erb | 1 + prism/templates/lib/prism/visitor.rb.erb | 1 + 23 files changed, 35 insertions(+), 12 deletions(-) diff --git a/lib/prism.rb b/lib/prism.rb index c7530e5874045d..079026cf89f5e4 100644 --- a/lib/prism.rb +++ b/lib/prism.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -# rbs_inline: enabled # :markup: markdown +#-- +# rbs_inline: enabled # The Prism Ruby parser. # diff --git a/lib/prism/desugar_compiler.rb b/lib/prism/desugar_compiler.rb index e08faa321919b0..c64d03f64ae1ec 100644 --- a/lib/prism/desugar_compiler.rb +++ b/lib/prism/desugar_compiler.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -# rbs_inline: enabled # :markup: markdown +#-- +# rbs_inline: enabled module Prism class DesugarAndWriteNode # :nodoc: diff --git a/lib/prism/lex_compat.rb b/lib/prism/lex_compat.rb index bdef99acfc9535..7f3f30b88da7f8 100644 --- a/lib/prism/lex_compat.rb +++ b/lib/prism/lex_compat.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -# rbs_inline: enabled # :markup: markdown +#-- +# rbs_inline: enabled module Prism # @rbs! diff --git a/lib/prism/node_ext.rb b/lib/prism/node_ext.rb index 57593a16125cd7..61f36e09bed8ef 100644 --- a/lib/prism/node_ext.rb +++ b/lib/prism/node_ext.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -# rbs_inline: enabled # :markup: markdown +#-- +# rbs_inline: enabled #-- # Here we are reopening the prism module to provide methods on nodes that aren't diff --git a/lib/prism/parse_result.rb b/lib/prism/parse_result.rb index e2e1145bdc60b9..9825d559aff9ba 100644 --- a/lib/prism/parse_result.rb +++ b/lib/prism/parse_result.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -# rbs_inline: enabled # :markup: markdown +#-- +# rbs_inline: enabled module Prism # @rbs! diff --git a/lib/prism/parse_result/comments.rb b/lib/prism/parse_result/comments.rb index 7b54ce7fd65386..df80792d39cd0e 100644 --- a/lib/prism/parse_result/comments.rb +++ b/lib/prism/parse_result/comments.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -# rbs_inline: enabled # :markup: markdown +#-- +# rbs_inline: enabled module Prism class ParseResult < Result diff --git a/lib/prism/parse_result/errors.rb b/lib/prism/parse_result/errors.rb index 03d65daecf0552..388309d23d2297 100644 --- a/lib/prism/parse_result/errors.rb +++ b/lib/prism/parse_result/errors.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -# rbs_inline: enabled # :markup: markdown +#-- +# rbs_inline: enabled require "stringio" diff --git a/lib/prism/parse_result/newlines.rb b/lib/prism/parse_result/newlines.rb index cfbc1ea1d0e97f..82e72bd564102e 100644 --- a/lib/prism/parse_result/newlines.rb +++ b/lib/prism/parse_result/newlines.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -# rbs_inline: enabled # :markup: markdown +#-- +# rbs_inline: enabled module Prism class ParseResult < Result diff --git a/lib/prism/pattern.rb b/lib/prism/pattern.rb index b15b04d9bc97c0..cbfb2c69b7267a 100644 --- a/lib/prism/pattern.rb +++ b/lib/prism/pattern.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -# rbs_inline: enabled # :markup: markdown +#-- +# rbs_inline: enabled module Prism # A pattern is an object that wraps a Ruby pattern matching expression. The diff --git a/lib/prism/relocation.rb b/lib/prism/relocation.rb index 2ac471d4250ebc..af0f7928272d8e 100644 --- a/lib/prism/relocation.rb +++ b/lib/prism/relocation.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -# rbs_inline: enabled # :markup: markdown +#-- +# rbs_inline: enabled module Prism # Prism parses deterministically for the same input. This provides a nice diff --git a/lib/prism/string_query.rb b/lib/prism/string_query.rb index c0dee63d3f9911..99ce57e5fec4c6 100644 --- a/lib/prism/string_query.rb +++ b/lib/prism/string_query.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -# rbs_inline: enabled # :markup: markdown +#-- +# rbs_inline: enabled module Prism # Query methods that allow categorizing strings based on their context for diff --git a/lib/prism/translation.rb b/lib/prism/translation.rb index d1afa5d8c54a24..5a086a75423443 100644 --- a/lib/prism/translation.rb +++ b/lib/prism/translation.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -# rbs_inline: enabled # :markup: markdown +#-- +# rbs_inline: enabled module Prism # This module is responsible for converting the prism syntax tree into other diff --git a/lib/prism/translation/parser_current.rb b/lib/prism/translation/parser_current.rb index 2dd018627c2559..f7c1070e30c80e 100644 --- a/lib/prism/translation/parser_current.rb +++ b/lib/prism/translation/parser_current.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true # :markup: markdown +#-- # typed: ignore module Prism diff --git a/prism/templates/lib/prism/compiler.rb.erb b/prism/templates/lib/prism/compiler.rb.erb index 16035b5456135b..13317cac0449e2 100644 --- a/prism/templates/lib/prism/compiler.rb.erb +++ b/prism/templates/lib/prism/compiler.rb.erb @@ -1,3 +1,4 @@ +#-- # rbs_inline: enabled module Prism diff --git a/prism/templates/lib/prism/dispatcher.rb.erb b/prism/templates/lib/prism/dispatcher.rb.erb index f1bd80a7cdcf2b..5991b0c9045bed 100644 --- a/prism/templates/lib/prism/dispatcher.rb.erb +++ b/prism/templates/lib/prism/dispatcher.rb.erb @@ -1,3 +1,4 @@ +#-- # rbs_inline: enabled module Prism diff --git a/prism/templates/lib/prism/dot_visitor.rb.erb b/prism/templates/lib/prism/dot_visitor.rb.erb index 3e37cf1a57b2e7..bdac9cfd3c6c0d 100644 --- a/prism/templates/lib/prism/dot_visitor.rb.erb +++ b/prism/templates/lib/prism/dot_visitor.rb.erb @@ -1,3 +1,4 @@ +#-- # rbs_inline: enabled require "cgi/escape" diff --git a/prism/templates/lib/prism/dsl.rb.erb b/prism/templates/lib/prism/dsl.rb.erb index de265a095b07c5..a75b8b253e240b 100644 --- a/prism/templates/lib/prism/dsl.rb.erb +++ b/prism/templates/lib/prism/dsl.rb.erb @@ -1,3 +1,4 @@ +#-- # rbs_inline: enabled module Prism diff --git a/prism/templates/lib/prism/inspect_visitor.rb.erb b/prism/templates/lib/prism/inspect_visitor.rb.erb index 9b15cb3d8abc60..820f5ae75fd7ee 100644 --- a/prism/templates/lib/prism/inspect_visitor.rb.erb +++ b/prism/templates/lib/prism/inspect_visitor.rb.erb @@ -1,3 +1,4 @@ +#-- # rbs_inline: enabled module Prism diff --git a/prism/templates/lib/prism/mutation_compiler.rb.erb b/prism/templates/lib/prism/mutation_compiler.rb.erb index 6fcbb25dcc90df..2d555048d2e102 100644 --- a/prism/templates/lib/prism/mutation_compiler.rb.erb +++ b/prism/templates/lib/prism/mutation_compiler.rb.erb @@ -1,3 +1,4 @@ +#-- # rbs_inline: enabled module Prism diff --git a/prism/templates/lib/prism/node.rb.erb b/prism/templates/lib/prism/node.rb.erb index 195200e4824d6d..2e42eeea13104d 100644 --- a/prism/templates/lib/prism/node.rb.erb +++ b/prism/templates/lib/prism/node.rb.erb @@ -1,3 +1,4 @@ +#-- # rbs_inline: enabled module Prism diff --git a/prism/templates/lib/prism/reflection.rb.erb b/prism/templates/lib/prism/reflection.rb.erb index d5ace99259ba96..0012f120b2bf01 100644 --- a/prism/templates/lib/prism/reflection.rb.erb +++ b/prism/templates/lib/prism/reflection.rb.erb @@ -1,3 +1,4 @@ +#-- # rbs_inline: enabled module Prism diff --git a/prism/templates/lib/prism/serialize.rb.erb b/prism/templates/lib/prism/serialize.rb.erb index 229322291e42c6..433b5207883e2e 100644 --- a/prism/templates/lib/prism/serialize.rb.erb +++ b/prism/templates/lib/prism/serialize.rb.erb @@ -1,3 +1,4 @@ +#-- # rbs_inline: enabled require "stringio" diff --git a/prism/templates/lib/prism/visitor.rb.erb b/prism/templates/lib/prism/visitor.rb.erb index 78726c53f1324e..f23e87d99ec9b2 100644 --- a/prism/templates/lib/prism/visitor.rb.erb +++ b/prism/templates/lib/prism/visitor.rb.erb @@ -1,3 +1,4 @@ +#-- # rbs_inline: enabled module Prism From 027131973fe4e7fc11f87f930f5c2a4719d9d66d Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 22 Feb 2026 16:40:04 -0500 Subject: [PATCH 3/8] [ruby/prism] Generate RBI from RBS https://github.com/ruby/prism/commit/2c88ed893f --- lib/prism.rb | 4 +- lib/prism/lex_compat.rb | 9 ++ lib/prism/node_ext.rb | 3 +- lib/prism/parse_result/newlines.rb | 24 +++--- lib/prism/pattern.rb | 4 +- lib/prism/prism.gemspec | 32 ++++--- prism/templates/lib/prism/dot_visitor.rb.erb | 2 +- prism/templates/lib/prism/node.rb.erb | 14 +-- prism/templates/template.rb | 89 +------------------- 9 files changed, 58 insertions(+), 123 deletions(-) diff --git a/lib/prism.rb b/lib/prism.rb index 079026cf89f5e4..6b34ab12bfc6e5 100644 --- a/lib/prism.rb +++ b/lib/prism.rb @@ -43,12 +43,12 @@ class CurrentVersionError < ArgumentError #: (String version) -> void def initialize(version) message = +"invalid version: Requested to parse as `version: 'current'`; " - segments = + major, minor, = if version.match?(/\A\d+\.\d+.\d+\z/) version.split(".").map(&:to_i) end - if segments && ((segments[0] < 3) || (segments[0] == 3 && segments[1] < 3)) + if major && minor && ((major < 3) || (major == 3 && minor < 3)) message << " #{version} is below the minimum supported syntax." else message << " #{version} is unknown. Please update the `prism` gem." diff --git a/lib/prism/lex_compat.rb b/lib/prism/lex_compat.rb index 7f3f30b88da7f8..99d8daacdd26cf 100644 --- a/lib/prism/lex_compat.rb +++ b/lib/prism/lex_compat.rb @@ -7,7 +7,16 @@ module Prism # @rbs! # module Translation # class Ripper + # EXPR_NONE: Integer # EXPR_BEG: Integer + # EXPR_MID: Integer + # EXPR_END: Integer + # EXPR_CLASS: Integer + # EXPR_VALUE: Integer + # EXPR_ARG: Integer + # EXPR_CMDARG: Integer + # EXPR_ENDARG: Integer + # EXPR_ENDFN: Integer # # class Lexer < Ripper # class State diff --git a/lib/prism/node_ext.rb b/lib/prism/node_ext.rb index 61f36e09bed8ef..4457c26fbe04cf 100644 --- a/lib/prism/node_ext.rb +++ b/lib/prism/node_ext.rb @@ -11,8 +11,7 @@ module Prism class Node #: (*String replacements) -> void def deprecated(*replacements) # :nodoc: - location = caller_locations(1, 1) - location = location[0].label if location + location = caller_locations(1, 1)&.[](0)&.label suggest = replacements.map { |replacement| "#{self.class}##{replacement}" } warn(<<~MSG, uplevel: 1, category: :deprecated) diff --git a/lib/prism/parse_result/newlines.rb b/lib/prism/parse_result/newlines.rb index 82e72bd564102e..450c7902266df8 100644 --- a/lib/prism/parse_result/newlines.rb +++ b/lib/prism/parse_result/newlines.rb @@ -114,56 +114,56 @@ def newline_flag!(lines) # :nodoc: end class BeginNode < Node - # @rbs override + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: # Never mark BeginNode with a newline flag, mark children instead. end end class ParenthesesNode < Node - # @rbs override + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: # Never mark ParenthesesNode with a newline flag, mark children instead. end end class IfNode < Node - # @rbs override + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: predicate.newline_flag!(lines) end end class UnlessNode < Node - # @rbs override + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: predicate.newline_flag!(lines) end end class UntilNode < Node - # @rbs override + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: predicate.newline_flag!(lines) end end class WhileNode < Node - # @rbs override + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: predicate.newline_flag!(lines) end end class RescueModifierNode < Node - # @rbs override + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: expression.newline_flag!(lines) end end class InterpolatedMatchLastLineNode < Node - # @rbs override + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: first = parts.first first.newline_flag!(lines) if first @@ -171,7 +171,7 @@ def newline_flag!(lines) # :nodoc: end class InterpolatedRegularExpressionNode < Node - # @rbs override + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: first = parts.first first.newline_flag!(lines) if first @@ -179,7 +179,7 @@ def newline_flag!(lines) # :nodoc: end class InterpolatedStringNode < Node - # @rbs override + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: first = parts.first first.newline_flag!(lines) if first @@ -187,7 +187,7 @@ def newline_flag!(lines) # :nodoc: end class InterpolatedSymbolNode < Node - # @rbs override + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: first = parts.first first.newline_flag!(lines) if first @@ -195,7 +195,7 @@ def newline_flag!(lines) # :nodoc: end class InterpolatedXStringNode < Node - # @rbs override + #: (Array[bool] lines) -> void def newline_flag!(lines) # :nodoc: first = parts.first first.newline_flag!(lines) if first diff --git a/lib/prism/pattern.rb b/lib/prism/pattern.rb index cbfb2c69b7267a..be0493df053677 100644 --- a/lib/prism/pattern.rb +++ b/lib/prism/pattern.rb @@ -94,9 +94,9 @@ def compile # matches the pattern. If no block is given, an enumerator will be returned # that will yield each node that matches the pattern. #-- - #: (node root) { (node) -> void } -> void #: (node root) -> Enumerator[node, void] - def scan(root) + #: (node root) { (node) -> void } -> void + def scan(root, &blk) return to_enum(:scan, root) unless block_given? @compiled ||= compile diff --git a/lib/prism/prism.gemspec b/lib/prism/prism.gemspec index f3cba41a4338bf..ca2db717baffc2 100644 --- a/lib/prism/prism.gemspec +++ b/lib/prism/prism.gemspec @@ -110,19 +110,31 @@ Gem::Specification.new do |spec| "lib/prism/translation/ruby_parser.rb", "lib/prism/visitor.rb", "prism.gemspec", - "rbi/prism.rbi", - "rbi/prism/compiler.rbi", - "rbi/prism/dsl.rbi", - "rbi/prism/inspect_visitor.rbi", - "rbi/prism/node_ext.rbi", - "rbi/prism/node.rbi", - "rbi/prism/parse_result.rbi", - "rbi/prism/reflection.rbi", - "rbi/prism/string_query.rbi", + "rbi/generated/prism.rbi", + "rbi/generated/prism/compiler.rbi", + "rbi/generated/prism/desugar_compiler.rbi", + "rbi/generated/prism/dispatcher.rbi", + "rbi/generated/prism/dot_visitor.rbi", + "rbi/generated/prism/dsl.rbi", + "rbi/generated/prism/inspect_visitor.rbi", + "rbi/generated/prism/lex_compat.rbi", + "rbi/generated/prism/mutation_compiler.rbi", + "rbi/generated/prism/node.rbi", + "rbi/generated/prism/node_ext.rbi", + "rbi/generated/prism/parse_result.rbi", + "rbi/generated/prism/pattern.rbi", + "rbi/generated/prism/reflection.rbi", + "rbi/generated/prism/relocation.rbi", + "rbi/generated/prism/serialize.rbi", + "rbi/generated/prism/string_query.rbi", + "rbi/generated/prism/translation.rbi", + "rbi/generated/prism/visitor.rbi", + "rbi/generated/prism/parse_result/comments.rbi", + "rbi/generated/prism/parse_result/errors.rbi", + "rbi/generated/prism/parse_result/newlines.rbi", "rbi/prism/translation/parser.rbi", "rbi/prism/translation/parser_versions.rbi", "rbi/prism/translation/ripper.rbi", - "rbi/prism/visitor.rbi", "sig/generated/prism.rbs", "sig/generated/prism/compiler.rbs", "sig/generated/prism/desugar_compiler.rbs", diff --git a/prism/templates/lib/prism/dot_visitor.rb.erb b/prism/templates/lib/prism/dot_visitor.rb.erb index bdac9cfd3c6c0d..88ef1e1f36b18e 100644 --- a/prism/templates/lib/prism/dot_visitor.rb.erb +++ b/prism/templates/lib/prism/dot_visitor.rb.erb @@ -126,7 +126,7 @@ module Prism end <%- nodes.each do |node| -%> - # (<%= node.name %>) -> void + #: (<%= node.name %>) -> void def visit_<%= node.human %>(node) # :nodoc: table = Table.new("<%= node.name %>") id = node_id(node) diff --git a/prism/templates/lib/prism/node.rb.erb b/prism/templates/lib/prism/node.rb.erb index 2e42eeea13104d..817b59b477b529 100644 --- a/prism/templates/lib/prism/node.rb.erb +++ b/prism/templates/lib/prism/node.rb.erb @@ -294,7 +294,7 @@ module Prism # node.breadth_first_search { |node| node.node_id == node_id } #-- #: () { (node) -> bool } -> node? - def breadth_first_search(&block) + def breadth_first_search(&blk) queue = [self] #: Array[node] while (node = queue.shift) @@ -313,7 +313,7 @@ module Prism # node.breadth_first_search_all { |node| node.is_a?(Prism::CallNode) } #-- #: () { (node) -> bool } -> Array[node] - def breadth_first_search_all(&block) + def breadth_first_search_all(&blk) queue = [self] #: Array[Prism::node] results = [] #: Array[Prism::node] @@ -366,9 +366,9 @@ module Prism # an enumerator that contains each child node. Excludes any `nil`s in # the place of optional nodes that were not present. #-- - #: () { (node) -> void } -> void #: () -> Enumerator[node, void] - def each_child_node + #: () { (node) -> void } -> void + def each_child_node(&blk) raise NoMethodError, "undefined method `each_child_node' for #{inspect}" end @@ -481,9 +481,9 @@ module Prism # See Node.each_child_node. #-- - #: () { (node) -> void } -> void #: () -> Enumerator[node, void] - def each_child_node + #: () { (node) -> void } -> void + def each_child_node(&blk) return to_enum(:each_child_node) unless block_given? <%- node.fields.each do |field| -%> @@ -705,7 +705,7 @@ module Prism <%- end -%> # :section: - #: (untyped other) -> bool + #: (untyped other) -> boolish def ===(other) # :nodoc: other.is_a?(<%= node.name %>)<%= " &&" if (fields = [*node.flags, *node.fields]).any? %> <%- fields.each_with_index do |field, index| -%> diff --git a/prism/templates/template.rb b/prism/templates/template.rb index a39f8bbb784297..e571c58bf298a8 100755 --- a/prism/templates/template.rb +++ b/prism/templates/template.rb @@ -166,16 +166,6 @@ def call_seq_type end end - def rbi_class - if specific_kind - "Prism::#{specific_kind}" - elsif union_kind - "T.any(#{union_kind.map { |kind| "Prism::#{kind}" }.join(", ")})" - else - "Prism::Node" - end - end - def check_field_kind if union_kind "[#{union_kind.join(', ')}].include?(#{name}.class)" @@ -208,16 +198,6 @@ def call_seq_type end end - def rbi_class - if specific_kind - "T.nilable(Prism::#{specific_kind})" - elsif union_kind - "T.nilable(T.any(#{union_kind.map { |kind| "Prism::#{kind}" }.join(", ")}))" - else - "T.nilable(Prism::Node)" - end - end - def check_field_kind if union_kind "[#{union_kind.join(', ')}, NilClass].include?(#{name}.class)" @@ -254,16 +234,6 @@ def call_seq_type end end - def rbi_class - if specific_kind - "T::Array[Prism::#{specific_kind}]" - elsif union_kind - "T::Array[T.any(#{union_kind.map { |kind| "Prism::#{kind}" }.join(", ")})]" - else - "T::Array[Prism::Node]" - end - end - def java_type "#{super}[]" end @@ -288,10 +258,6 @@ def call_seq_type "Symbol" end - def rbi_class - "Symbol" - end - def java_type JAVA_STRING_TYPE end @@ -308,10 +274,6 @@ def call_seq_type "Symbol | nil" end - def rbi_class - "T.nilable(Symbol)" - end - def java_type JAVA_STRING_TYPE end @@ -328,10 +290,6 @@ def call_seq_type "Array[Symbol]" end - def rbi_class - "T::Array[Symbol]" - end - def java_type "#{JAVA_STRING_TYPE}[]" end @@ -347,10 +305,6 @@ def call_seq_type "String" end - def rbi_class - "String" - end - def java_type "byte[]" end @@ -370,10 +324,6 @@ def call_seq_type "Location" end - def rbi_class - "Prism::Location" - end - def java_type "Location" end @@ -393,10 +343,6 @@ def call_seq_type "Location | nil" end - def rbi_class - "T.nilable(Prism::Location)" - end - def java_type "Location" end @@ -412,10 +358,6 @@ def call_seq_type "Integer" end - def rbi_class - "Integer" - end - def java_type "int" end @@ -431,10 +373,6 @@ def call_seq_type "Integer" end - def rbi_class - "Integer" - end - def java_type "int" end @@ -451,10 +389,6 @@ def call_seq_type "Integer" end - def rbi_class - "Integer" - end - def java_type "Object" end @@ -471,10 +405,6 @@ def call_seq_type "Float" end - def rbi_class - "Float" - end - def java_type "double" end @@ -629,8 +559,7 @@ def render(name, write_to: nil) extension = File.extname(filepath.gsub(".erb", "")) heading = - case extension - when ".rb" + if extension == ".rb" <<~HEADING # frozen_string_literal: true # :markup: markdown @@ -643,17 +572,6 @@ def render(name, write_to: nil) ++ =end - HEADING - when ".rbi" - <<~HEADING - # typed: strict - - =begin - This file is generated by the templates/template.rb script and should not be - modified manually. See #{filepath} - if you are looking to modify the template - =end - HEADING else <<~HEADING @@ -738,10 +656,7 @@ def locals "src/node.c", "src/prettyprint.c", "src/serialize.c", - "src/token_type.c", - "rbi/prism/dsl.rbi", - "rbi/prism/node.rbi", - "rbi/prism/visitor.rbi" + "src/token_type.c" ] end end From b52cb82d3c7b2aacdc7b8a452ffe74dbef54db26 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 25 Feb 2026 12:08:36 -0500 Subject: [PATCH 4/8] ZJIT: Format negative offsets as small negative hex values (#16248) Before: Optimized HIR: fn block in
@benchmarks/setivar.rb:40: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): v10:CPtr = GetEP 1 v11:BasicObject = LoadField v10, :obj@0xffffffffffffffe8 <= PatchPoint NoSingletonClass(TheClass@0x1037633c0) PatchPoint MethodRedefined(TheClass@0x1037633c0, set_value_loop@0xf5f1, cme:0x103a14650) v21:HeapObject[class_exact:TheClass] = GuardType v11, HeapObject[class_exact:TheClass] v22:BasicObject = SendDirect v21, 0x16cf41630, :set_value_loop (0x16cf41670) CheckInterrupts Return v22 After: Optimized HIR: fn block in
@benchmarks/setivar.rb:40: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): v10:CPtr = GetEP 1 v11:BasicObject = LoadField v10, :obj@-0x18 <= PatchPoint NoSingletonClass(TheClass@0x102bc3420) PatchPoint MethodRedefined(TheClass@0x102bc3420, set_value_loop@0xf5f1, cme:0x102e945f0) v21:HeapObject[class_exact:TheClass] = GuardType v11, HeapObject[class_exact:TheClass] v22:BasicObject = SendDirect v21, 0x16dadd630, :set_value_loop (0x16dadd670) CheckInterrupts Return v22 --- zjit/src/hir.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index a1fc0e90cff8b2..e188f4aca89826 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -431,6 +431,16 @@ impl PtrPrintMap { } } +struct Offset(i32); + +impl std::fmt::LowerHex for Offset { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let prefix = if f.alternate() { "0x" } else { "" }; + let bare_hex = format!("{:x}", self.0.abs()); + f.pad_integral(self.0 >= 0, prefix, &bare_hex) + } +} + impl PtrPrintMap { /// Map a pointer for printing pub fn map_ptr(&self, ptr: *const T) -> *const T { @@ -467,8 +477,8 @@ impl PtrPrintMap { self.map_ptr(id as *const c_void) } - fn map_offset(&self, id: i32) -> *const c_void { - self.map_ptr(id as *const c_void) + fn map_offset(&self, id: i32) -> Offset { + Offset(self.map_ptr(id as *const c_void) as i32) } /// Map shape ID into a pointer for printing @@ -1644,8 +1654,8 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { &Insn::GetEP { level } => write!(f, "GetEP {level}"), Insn::GetLEP => write!(f, "GetLEP"), Insn::LoadSelf => write!(f, "LoadSelf"), - &Insn::LoadField { recv, id, offset, return_type: _ } => write!(f, "LoadField {recv}, :{}@{:p}", id.contents_lossy(), self.ptr_map.map_offset(offset)), - &Insn::StoreField { recv, id, offset, val } => write!(f, "StoreField {recv}, :{}@{:p}, {val}", id.contents_lossy(), self.ptr_map.map_offset(offset)), + &Insn::LoadField { recv, id, offset, return_type: _ } => write!(f, "LoadField {recv}, :{}@{:#x}", id.contents_lossy(), self.ptr_map.map_offset(offset)), + &Insn::StoreField { recv, id, offset, val } => write!(f, "StoreField {recv}, :{}@{:#x}, {val}", id.contents_lossy(), self.ptr_map.map_offset(offset)), &Insn::WriteBarrier { recv, val } => write!(f, "WriteBarrier {recv}, {val}"), Insn::SetIvar { self_val, id, val, .. } => write!(f, "SetIvar {self_val}, :{}, {val}", id.contents_lossy()), Insn::GetGlobal { id, .. } => write!(f, "GetGlobal :{}", id.contents_lossy()), From 2168fdbdd4f869008aaafea4b7a83a012eee4184 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Wed, 25 Feb 2026 16:23:02 +0000 Subject: [PATCH 5/8] Move class pre-aging out of the allocation path Previously classes and modules were pre-aged. Ie. as soon as they're allocated they are aged to old_age - 1. This was done with the assumption that classes are generally always long lived, so we should assume that any that survive a single GC can be considered old. This commit keeps the same semantics, but moves the logic out of the allocation path, in order to simplify allocation. Classes and modules are now set to old-age the first time they are marked. --- gc/default/default.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 046aa146f73055..1099d6e0dc11e5 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -2175,10 +2175,6 @@ newobj_init(VALUE klass, VALUE flags, int wb_protected, rb_objspace_t *objspace, RBASIC(obj)->shape_id = 0; #endif - int t = flags & RUBY_T_MASK; - if (t == T_CLASS || t == T_MODULE || t == T_ICLASS) { - RVALUE_AGE_SET_CANDIDATE(objspace, obj); - } #if RACTOR_CHECK_MODE void rb_ractor_setup_belonging(VALUE obj); @@ -4407,8 +4403,16 @@ gc_aging(rb_objspace_t *objspace, VALUE obj) if (!RVALUE_PAGE_WB_UNPROTECTED(page, obj)) { if (!RVALUE_OLD_P(objspace, obj)) { - gc_report(3, objspace, "gc_aging: YOUNG: %s\n", rb_obj_info(obj)); - RVALUE_AGE_INC(objspace, obj); + int t = BUILTIN_TYPE(obj); + if (t == T_CLASS || t == T_MODULE || t == T_ICLASS) { + gc_report(3, objspace, "gc_aging: YOUNG class: %s\n", rb_obj_info(obj)); + RVALUE_AGE_SET(obj, RVALUE_OLD_AGE); + RVALUE_OLD_UNCOLLECTIBLE_SET(objspace, obj); + } + else { + gc_report(3, objspace, "gc_aging: YOUNG: %s\n", rb_obj_info(obj)); + RVALUE_AGE_INC(objspace, obj); + } } else if (is_full_marking(objspace)) { GC_ASSERT(RVALUE_PAGE_UNCOLLECTIBLE(page, obj) == FALSE); From ad380f8752c93e4c763329825ebf271e583b2b3c Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Wed, 25 Feb 2026 10:58:02 +0000 Subject: [PATCH 6/8] Correct timezone type: It's Asia/Tokyo, not Tokyo/Asia https://timezonedb.com/time-zones/Asia/Tokyo --- .github/workflows/check_misc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 1be94a016e4126..e900f0912372a2 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -70,7 +70,7 @@ jobs: run: | date +"mon=%-m"%n"day=%-d" >> $GITHUB_OUTPUT env: - TZ: Tokyo/Asia + TZ: Asia/Tokyo - id: deprecation run: | From b3a1286d5c921da8ee9a1cee05b3d4c3d588efee Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Tue, 24 Feb 2026 11:58:47 -0800 Subject: [PATCH 7/8] Avoid duplicate struct name in rb_raw_obj_info Previously we printed the struct name of T_DATA both in rb_raw_obj_info_buitin_type and in rb_raw_obj_info_common via type_name. --- gc.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gc.c b/gc.c index 2a4dec32e9119f..1af2ba41e7bf73 100644 --- a/gc.c +++ b/gc.c @@ -5054,12 +5054,6 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU APPEND_F("r:%d", r->pub.id); } } - else { - const char * const type_name = rb_objspace_data_type_name(obj); - if (type_name) { - APPEND_F("%s", type_name); - } - } break; } case T_IMEMO: { From 9ab1dfadc9ec2b435875b4d2ec98479f7defb690 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 25 Feb 2026 13:44:16 -0800 Subject: [PATCH 8/8] ZJIT: Decouple gen_function_stub and gen_function_stub_hit_trampoline (#16249) Before this change, gen_function_stub and gen_function_stub_hit_trampoline communicated via a scratch register. We would like gen_function_stub_hit_trampoline to have more freedom with regard to the registers it uses, especially for the CCall in to function_stub_hit. Instead of communicating via scratch register, we'll communicate via stack. Practically speaking, this means: * Stop using x15 (scratch reg) to communicate iseq call addr from call stub to function sub hit trampoline; use stack instead * Don't try to CCall with x15 as first argument; can't use scratch reg in parallel move of arguments Here is pseudo assembly of before this commit: ``` some_send_direct_in_a_ruby_method(JIT code): mov x15, gen_function_stub mov x0, self mov x1, 1 blr x15 gen_function_stub: mov x15, 0xISEQADDR (the address of the ISEQ we _want_ to compile) jmp function_stub_hit_trampoline function_stub_hit_trampoline: function prologue cpush ALL_JIT_REGS mov x0, x15 # currently x15 is 0xISEQADDR mov x1, CFP mov x2, SP blr function_stub_hit mov x15, x0 # write jump address to x15 (code pointer for compiled iseq) cpop ALL_JIT_REGS function epilogue jmp x15 ``` Here is pseudo assembly of after this commit: ``` some_send_direct_in_a_ruby_method(JIT code): mov x15, gen_function_stub mov x0, self mov x1, 1 blr x15 gen_function_stub: mov x15, 0xISEQADDR (the address of the ISEQ we _want_ to compile) push x15 jmp function_stub_hit_trampoline function_stub_hit_trampoline: pop x15 # get the ISEQ addr from gen_function_stub function prologue cpush ALL_JIT_REGS mov x0, x15 # currently x15 is 0xISEQADDR mov x1, CFP mov x2, SP blr function_stub_hit mov x15, x0 # write jump address to x15 (code pointer for compiled iseq) cpop ALL_JIT_REGS function epilogue jmp x15 ``` --- zjit/src/codegen.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 6a705b90dbdaff..c1c5b72690adc5 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -2815,6 +2815,7 @@ fn gen_function_stub(cb: &mut CodeBlock, iseq_call: IseqCallRef) -> Result Result Result