Skip to content

Commit ef9a109

Browse files
committed
Label T::Struct props with immutable: true as readonly
1 parent 9c70a6a commit ef9a109

File tree

7 files changed

+35
-19
lines changed

7 files changed

+35
-19
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
## main
44

5-
###
5+
### Bug Fixes
66

77
* Handle multiple invocations of `mixes_in_class_methods` within a class
8+
* Label `T::Struct` `prop`s with `immutable: true` as `readonly`
89

910
## 0.7.0 (2022-08-24)
1011

lib/yard-sorbet/handlers/enums_handler.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def process
2727

2828
sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Boolean) }
2929
def const_assign_node?(node)
30-
node.type == :assign && node[0][0].type == :const
30+
node.type == :assign && node.dig(0, 0).type == :const
3131
end
3232
end
3333
end

lib/yard-sorbet/handlers/sig_handler.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ def parse_params(method_node, node, docstring)
4848
return if ATTR_NODE_TYPES.include?(method_node.type)
4949

5050
sibling = NodeUtils.sibling_node(node)
51-
sibling[0][0].each do |param|
52-
param_name = param[0][0]
51+
sibling.dig(0, 0).each do |param|
52+
param_name = param.dig(0, 0)
5353
types = SigToYARD.convert(param.last)
5454
TagUtils.upsert_tag(docstring, 'param', types, param_name)
5555
end

lib/yard-sorbet/handlers/struct_prop_handler.rb

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class StructPropHandler < YARD::Handlers::Ruby::Base
1313

1414
sig { void }
1515
def process
16-
name = statement.parameters.first.last.last.source
16+
name = params.dig(0, -1, -1).source
1717
prop = make_prop(name)
1818
update_state(prop)
1919
object = YARD::CodeObjects::MethodObject.new(namespace, name, scope)
@@ -27,36 +27,45 @@ def process
2727
sig { params(object: YARD::CodeObjects::MethodObject, prop: TStructProp).void }
2828
def decorate_object(object, prop)
2929
object.source = prop.source
30-
# TODO: this should use `+` to delimit the attribute name when markdown is disabled
31-
reader_docstring = prop.doc.empty? ? "Returns the value of attribute `#{prop.prop_name}`." : prop.doc
30+
# TODO: this should use `+` to delimit the prop name when markdown is disabled
31+
reader_docstring = prop.doc.empty? ? "Returns the value of prop `#{prop.prop_name}`." : prop.doc
3232
docstring = YARD::DocstringParser.new.parse(reader_docstring).to_docstring
3333
docstring.add_tag(YARD::Tags::Tag.new(:return, '', prop.types))
3434
object.docstring = docstring.to_raw
3535
end
3636

37-
# Get the default prop value
38-
sig { returns(T.nilable(String)) }
39-
def default_value
40-
statement.traverse { break _1 if _1.type == :label && _1.source == 'default:' }&.parent&.[](1)&.source
37+
sig { returns(T::Boolean) }
38+
def immutable?
39+
statement.method_name(true) == :const || kw_arg('immutable:') == 'true'
40+
end
41+
42+
# @return the value passed to the keyword argument, or nil
43+
sig { params(kwd: String).returns(T.nilable(String)) }
44+
def kw_arg(kwd)
45+
params[2]&.find { _1.jump(:label).source == kwd }&.[](1)&.source
4146
end
4247

4348
sig { params(name: String).returns(TStructProp) }
4449
def make_prop(name)
4550
TStructProp.new(
46-
default: default_value,
51+
default: kw_arg('default:'),
4752
doc: statement.docstring.to_s,
4853
prop_name: name,
4954
source: statement.source,
50-
types: SigToYARD.convert(statement.parameters[1])
55+
types: SigToYARD.convert(params[1])
5156
)
5257
end
5358

59+
sig { returns(T::Array[T.untyped]) }
60+
def params
61+
@params ||= T.let(statement.parameters(false), T.nilable(T::Array[T.untyped]))
62+
end
63+
5464
# Register the field explicitly as an attribute.
55-
# While `const` attributes are immutable, `prop` attributes may be reassigned.
5665
sig { params(object: YARD::CodeObjects::MethodObject, name: String).void }
5766
def register_attrs(object, name)
58-
# Create the virtual method in our current scope
59-
write = statement.method_name(true) == :prop ? object : nil
67+
write = immutable? ? nil : object
68+
# Create the virtual attribute in our current scope
6069
namespace.attributes[scope][name] ||= SymbolHash[read: object, write: write]
6170
end
6271

lib/yard-sorbet/sig_to_yard.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def self.convert(node)
8888
sig { params(node: YARD::Parser::Ruby::AstNode).returns([String]) }
8989
private_class_method def self.convert_collection(node)
9090
collection_type = node.first.source.split('::').last
91-
member_type = convert_node(node.last.first).join(', ')
91+
member_type = convert_node(node.dig(-1, 0)).join(', ')
9292
["#{collection_type}<#{member_type}>"]
9393
end
9494

@@ -116,7 +116,7 @@ def self.convert(node)
116116
# Order matters here, putting `nil` last results in a more concise return syntax in the UI (superscripted `?`):
117117
# https://github.com/lsegal/yard/blob/cfa62ae/lib/yard/templates/helpers/html_helper.rb#L499-L500
118118
when :nilable then convert_node(node.last).push('nil')
119-
when :any then node.last.first.children.flat_map { convert_node(_1) }
119+
when :any then node.dig(-1, 0).children.flat_map { convert_node(_1) }
120120
else [node.source]
121121
end
122122
end

spec/data/struct_handler.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class PersonStruct < T::Struct
1212
# A writable
1313
prop :writable, String
1414
const :mystery, T.untyped
15+
prop :not_mutable, Integer, default: 0, immutable: true
1516
end
1617

1718
class SpecializedPersonStruct < T::Struct

spec/yard_sorbet/handlers/struct_prop_handler_spec.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
it 'creates a docstring if it does not exist' do
2424
node = YARD::Registry.at('PersonStruct#mystery')
25-
expect(node.docstring).to eq('Returns the value of attribute `mystery`.')
25+
expect(node.docstring).to eq('Returns the value of prop `mystery`.')
2626
end
2727

2828
it 'handles default values appropriately' do
@@ -35,6 +35,11 @@
3535
expect(node.writer?).to be(false)
3636
end
3737

38+
it 'marks `immutable: true` attributes read-only' do
39+
node = YARD::Registry.at('PersonStruct#not_mutable')
40+
expect(node.writer?).to be(false)
41+
end
42+
3843
it 'does not mark `prop` attributes read-only' do
3944
node = YARD::Registry.at('PersonStruct#writable')
4045
expect(node.writer?).to be(true)

0 commit comments

Comments
 (0)