Skip to content

Commit b6693d8

Browse files
authored
Merge pull request #2635 from ruby/inline--constant
Inline constant declaration
2 parents 36847f1 + 8cd37de commit b6693d8

15 files changed

+905
-25
lines changed

docs/inline.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,67 @@ Instance variable declarations must be under the `class`/`module` syntax, and th
388388
### Current Limitations
389389

390390
- Only instance variables of class/module instances are allowed
391+
392+
## Constants
393+
394+
Constants are supported by inline RBS declaration.
395+
396+
```ruby
397+
Foo = 123
398+
399+
module Bar
400+
Baz = [1, ""] #: [Integer, String]
401+
end
402+
403+
# Version of the library
404+
#
405+
VERSION = "1.2.3".freeze #: String
406+
```
407+
408+
### Type Inference for Literal Constants
409+
410+
The types of constants may be automatically inferred when the right-hand side consists of literals:
411+
412+
- **Integers**: `COUNT = 42``Integer`
413+
- **Floats**: `RATE = 3.14``Float`
414+
- **Booleans**: `ENABLED = true``bool`
415+
- **Strings**: `NAME = "test"``String`
416+
- **Symbols**: `STATUS = :ready``:ready`
417+
418+
```ruby
419+
MAX_SIZE = 100 # Inferred as Integer
420+
PI = 3.14159 # Inferred as Float
421+
DEBUG = false # Inferred as bool
422+
APP_NAME = "MyApp" # Inferred as String
423+
DEFAULT_MODE = :strict # Inferred as :strict
424+
```
425+
426+
### Explicit Type Annotations
427+
428+
For more complex types or when you want to override inference, use the `#:` syntax:
429+
430+
```ruby
431+
ITEMS = [1, "hello"] #: [Integer, String]
432+
CONFIG = { name: "app", version: 1 } #: { name: String, version: Integer }
433+
CALLBACK = -> { puts "done" } #: ^() -> void
434+
```
435+
436+
### Documentation Comments
437+
438+
Comments above constant declarations become part of the constant's documentation:
439+
440+
```ruby
441+
# The maximum number of retries allowed
442+
# before giving up on the operation
443+
MAX_RETRIES = 3
444+
445+
# Application configuration loaded from environment
446+
#
447+
# This hash contains all the runtime configuration
448+
# settings for the application.
449+
CONFIG = load_config() #: Hash[String, untyped]
450+
```
451+
452+
### Current Limitations
453+
454+
- Module/class aliases are not supported

lib/rbs/ast/ruby/comment_block.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,23 @@ def leading_annotation?(index)
222222

223223
false
224224
end
225+
226+
def as_comment
227+
lines = [] #: Array[String]
228+
229+
each_paragraph([]) do |paragraph|
230+
case paragraph
231+
when Location
232+
lines << paragraph.local_source
233+
end
234+
end
235+
236+
string = lines.join("\n")
237+
238+
unless string.strip.empty?
239+
AST::Comment.new(string: string, location: location)
240+
end
241+
end
225242
end
226243
end
227244
end

lib/rbs/ast/ruby/declarations.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,59 @@ def name_location
129129
rbs_location(node.constant_path.location)
130130
end
131131
end
132+
133+
class ConstantDecl < Base
134+
attr_reader :leading_comment
135+
attr_reader :constant_name
136+
attr_reader :node
137+
attr_reader :type_annotation
138+
139+
def initialize(buffer, constant_name, node, leading_comment, type_annotation)
140+
super(buffer)
141+
@constant_name = constant_name
142+
@node = node
143+
@leading_comment = leading_comment
144+
@type_annotation = type_annotation
145+
end
146+
147+
def location
148+
rbs_location(node.location)
149+
end
150+
151+
def name_location
152+
case node
153+
when Prism::ConstantWriteNode
154+
rbs_location(node.name_loc)
155+
when Prism::ConstantPathWriteNode
156+
rbs_location(node.target.location)
157+
end
158+
end
159+
160+
def type
161+
return type_annotation.type if type_annotation
162+
163+
case node.value
164+
when Prism::IntegerNode
165+
BuiltinNames::Integer.instance_type
166+
when Prism::FloatNode
167+
BuiltinNames::Float.instance_type
168+
when Prism::StringNode
169+
BuiltinNames::String.instance_type
170+
when Prism::TrueNode, Prism::FalseNode
171+
Types::Bases::Bool.new(location: nil)
172+
when Prism::SymbolNode
173+
BuiltinNames::Symbol.instance_type
174+
when Prism::NilNode
175+
Types::Bases::Nil.new(location: nil)
176+
else
177+
Types::Bases::Any.new(location: nil)
178+
end
179+
end
180+
181+
def comment
182+
leading_comment&.as_comment
183+
end
184+
end
132185
end
133186
end
134187
end

lib/rbs/ast/ruby/helpers/constant_helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ def constant_as_type_name(node)
1515
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
1616
nil
1717
end
18+
when Prism::ConstantWriteNode
19+
TypeName.new(name: node.name, namespace: Namespace.empty)
20+
when Prism::ConstantPathWriteNode
21+
constant_as_type_name(node.target)
1822
end
1923
end
2024
end

lib/rbs/buffer.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def line_count
2929

3030
def ranges
3131
@ranges ||= begin
32-
if content.empty?
32+
if content.empty?
3333
ranges = [0...0] #: Array[Range[Integer]]
3434
lines = [""]
3535
else
@@ -89,9 +89,9 @@ def inspect
8989

9090
def rbs_location(location, loc2=nil)
9191
if loc2
92-
Location.new(self, location.start_character_offset, loc2.end_character_offset)
92+
Location.new(self.top_buffer, location.start_character_offset, loc2.end_character_offset)
9393
else
94-
Location.new(self, location.start_character_offset, location.end_character_offset)
94+
Location.new(self.top_buffer, location.start_character_offset, location.end_character_offset)
9595
end
9696
end
9797

lib/rbs/definition.rb

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,25 +66,7 @@ def comment
6666
when AST::Members::Base
6767
member.comment
6868
when AST::Ruby::Members::Base
69-
if member.leading_comment
70-
lines = [] #: Array[String]
71-
72-
member.leading_comment.each_paragraph([]) do |paragraph|
73-
case paragraph
74-
when Location
75-
lines << paragraph.local_source
76-
end
77-
end
78-
79-
string = lines.join("\n")
80-
81-
unless string.strip.empty?
82-
AST::Comment.new(
83-
string: string,
84-
location: member.leading_comment.location
85-
)
86-
end
87-
end
69+
member.leading_comment&.as_comment
8870
end
8971
end
9072

lib/rbs/environment.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,23 @@ def insert_ruby_decl(decl, context:, namespace:)
409409
decl.each_decl do |member|
410410
insert_ruby_decl(member, context: inner_context, namespace: name.to_namespace)
411411
end
412+
413+
when AST::Ruby::Declarations::ConstantDecl
414+
name = decl.constant_name.with_prefix(namespace)
415+
416+
if entry = constant_entry(name)
417+
case entry
418+
when ClassAliasEntry, ModuleAliasEntry, ConstantEntry
419+
raise DuplicatedDeclarationError.new(name, decl, entry.decl)
420+
when ClassEntry, ModuleEntry
421+
raise DuplicatedDeclarationError.new(name, decl, *entry.each_decl.to_a)
422+
end
423+
end
424+
425+
constant_decls[name] = ConstantEntry.new(name: name, decl: decl, context: context)
426+
427+
else
428+
raise "Unknown Ruby declaration type: #{decl.class}"
412429
end
413430
end
414431

@@ -717,6 +734,17 @@ def resolve_ruby_decl(resolver, decl, context:, prefix:)
717734
end
718735
end
719736

737+
when AST::Ruby::Declarations::ConstantDecl
738+
full_name = decl.constant_name.with_prefix(prefix)
739+
740+
AST::Ruby::Declarations::ConstantDecl.new(
741+
decl.buffer,
742+
full_name,
743+
decl.node,
744+
decl.leading_comment,
745+
decl.type_annotation&.map_type_name {|name, _, _| absolute_type_name(resolver, nil, name, context: context) }
746+
)
747+
720748
else
721749
raise "Unknown declaration type: #{decl.class}"
722750
end

lib/rbs/inline_parser.rb

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def initialize(location, message)
2929
NonConstantSuperClassName = _ = Class.new(Base)
3030
TopLevelMethodDefinition = _ = Class.new(Base)
3131
TopLevelAttributeDefinition = _ = Class.new(Base)
32+
NonConstantConstantDeclaration = _ = Class.new(Base)
3233
UnusedInlineAnnotation = _ = Class.new(Base)
3334
AnnotationSyntaxError = _ = Class.new(Base)
3435
MixinMultipleArguments = _ = Class.new(Base)
@@ -237,6 +238,19 @@ def visit_call_node(node)
237238
end
238239
end
239240

241+
def visit_constant_write_node(node)
242+
return if skip_node?(node)
243+
244+
# Parse constant declaration (both top-level and in classes/modules)
245+
parse_constant_declaration(node)
246+
end
247+
248+
def visit_constant_path_write_node(node)
249+
return if skip_node?(node)
250+
251+
parse_constant_declaration(node)
252+
end
253+
240254
def parse_mixin_call(node)
241255
# Check for multiple arguments
242256
if node.arguments && node.arguments.arguments.length > 1
@@ -353,6 +367,60 @@ def parse_attribute_call(node)
353367
current_module!.members << member
354368
end
355369

370+
def parse_constant_declaration(node)
371+
# Create TypeName for the constant
372+
unless constant_name = constant_as_type_name(node)
373+
location =
374+
case node
375+
when Prism::ConstantWriteNode
376+
node.name_loc
377+
when Prism::ConstantPathWriteNode
378+
node.target.location
379+
end
380+
381+
diagnostics << Diagnostic::NonConstantConstantDeclaration.new(
382+
rbs_location(location),
383+
"Constant name must be a constant"
384+
)
385+
return
386+
end
387+
388+
# Look for leading comment block
389+
leading_block = comments.leading_block!(node)
390+
report_unused_block(leading_block) if leading_block
391+
392+
# Look for trailing type annotation (#: Type)
393+
trailing_block = comments.trailing_block!(node.location)
394+
type_annotation = nil
395+
396+
if trailing_block
397+
case annotation = trailing_block.trailing_annotation([])
398+
when AST::Ruby::Annotations::NodeTypeAssertion
399+
type_annotation = annotation
400+
when AST::Ruby::CommentBlock::AnnotationSyntaxError
401+
diagnostics << Diagnostic::AnnotationSyntaxError.new(
402+
annotation.location, "Syntax error: " + annotation.error.error_message
403+
)
404+
end
405+
end
406+
407+
# Create the constant declaration
408+
constant_decl = AST::Ruby::Declarations::ConstantDecl.new(
409+
buffer,
410+
constant_name,
411+
node,
412+
leading_block,
413+
type_annotation
414+
)
415+
416+
# Insert the constant declaration appropriately
417+
if current_module
418+
current_module.members << constant_decl
419+
else
420+
result.declarations << constant_decl
421+
end
422+
end
423+
356424
def insert_declaration(decl)
357425
if current_module
358426
current_module.members << decl

sig/ast/ruby/comment_block.rbs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ module RBS
115115
def location: () -> Location
116116

117117
private def leading_annotation?: (Integer index) -> bool
118+
119+
# Returns an comment object that contains the docs of from the comment block
120+
#
121+
# It ignores type annotations and syntax errors.
122+
#
123+
def as_comment: () -> AST::Comment?
118124
end
119125
end
120126
end

0 commit comments

Comments
 (0)