Skip to content

Commit ef0782b

Browse files
doriableunmultimediostefanvanburen
authored
Split spans for nested message value elements (#4139)
Co-authored-by: Julian Figueroa <[email protected]> Co-authored-by: Stefan VanBuren <[email protected]>
1 parent fd3f634 commit ef0782b

File tree

2 files changed

+94
-4
lines changed

2 files changed

+94
-4
lines changed

private/buf/buflsp/file.go

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -622,15 +622,63 @@ func (f *file) importToSymbol(imp ir.Import) *symbol {
622622
}
623623
}
624624

625+
// messageToSymbols takes an [ir.MessageValue] and returns the symbols parsed from it.
625626
func (f *file) messageToSymbols(msg ir.MessageValue) []*symbol {
627+
return f.messageToSymbolsHelper(msg, 0, nil)
628+
}
629+
630+
// messageToSymbolsHelper is a recursive helper for extracting the symbols from a [ir.MessageValue].
631+
func (f *file) messageToSymbolsHelper(msg ir.MessageValue, index int, parents []*symbol) []*symbol {
626632
var symbols []*symbol
627633
for field := range msg.Fields() {
628634
if field.ValueAST().IsZero() {
629635
continue
630636
}
637+
// There are a couple of different ways options can be structured, e.g.
638+
//
639+
// [(option).message = {
640+
// field_a: 2
641+
// field_b: 100
642+
// }]
643+
//
644+
// Or:
645+
//
646+
// [
647+
// (option).message.field_a = 2
648+
// (option).message.field_b = 100
649+
// ]
650+
//
651+
// For both examples, we would want to create a separate symbol for each referenceable
652+
// part of each path, e.g. (option), .message, .field_a, etc.
653+
//
654+
// An option is represented as a [ir.MessageValue] that is accessed recursively, so in
655+
// both examples above, we have:
656+
//
657+
// (option) -> (option).message -> (option).message.field_a / field_a
658+
// -> (option).message.field_b / field_b
659+
//
660+
// As we walk the message recursively, we set a symbol for each message/field along the
661+
// way. Because we are accessing each message from the top-level message, we need to
662+
// make sure that we capture a symbol for each corresponding path span along the way.
663+
//
664+
// In the example, in the second definition, (option) and .message for field_b has a
665+
// separate span from (option) and .message for field_a, but when we walk the mesasge
666+
// tree, we get the span for (option) and .mesage for the first field. So we check the
667+
// symbols we've collected so far in parents and make sure we have captured a symbol for
668+
// each path component.
631669
for element := range seq.Values(field.Elements()) {
632-
span := element.Value().KeyASTs().At(element.ValueNodeIndex()).Span()
633-
elem := &symbol{
670+
key := field.KeyASTs().At(element.ValueNodeIndex())
671+
components := slices.Collect(key.AsPath().Components)
672+
var span report.Span
673+
// This covers the first case in the example above where the path is relative,
674+
// e.g. field_a is a relative path within { } for (option).message.
675+
if index > len(components)-1 {
676+
span = components[len(components)-1].Span()
677+
} else {
678+
// Otherwise, we get the component for the corresponding index.
679+
span = components[index].Span()
680+
}
681+
sym := &symbol{
634682
// NOTE: no [ir.Symbol] for option elements
635683
file: f,
636684
span: span,
@@ -640,9 +688,48 @@ func (f *file) messageToSymbols(msg ir.MessageValue) []*symbol {
640688
},
641689
isOption: true,
642690
}
643-
symbols = append(symbols, elem)
691+
symbols = append(symbols, sym)
644692
if !element.AsMessage().IsZero() {
645-
symbols = append(symbols, f.messageToSymbols(element.AsMessage())...)
693+
// For message value elements, we use the Type AST.
694+
sym.kind = &reference{
695+
def: element.Type().AST(),
696+
fullName: element.Type().FullName(),
697+
}
698+
symbols = append(symbols, f.messageToSymbolsHelper(element.AsMessage(), index+1, symbols)...)
699+
}
700+
701+
// We check back along the path to make sure that we have a symbol for each component.
702+
//
703+
// We need to ensure that (option) for (option).message.field_b has a symbol defined
704+
// among the parent symbols.
705+
if len(components) > 1 {
706+
parentType := element.Value().Container()
707+
for _, component := range slices.Backward(components) {
708+
if component.IsLast() {
709+
continue
710+
}
711+
found := false
712+
for _, parent := range parents {
713+
if parent.span == component.Span() {
714+
found = true
715+
break
716+
}
717+
}
718+
if !found {
719+
sym := &symbol{
720+
// NOTE: no [ir.Symbol] for option elements
721+
file: f,
722+
span: component.Span(),
723+
kind: &reference{
724+
def: parentType.Type().AST(),
725+
fullName: parentType.Type().FullName(),
726+
},
727+
isOption: true,
728+
}
729+
symbols = append(symbols, sym)
730+
}
731+
parentType = parentType.AsValue().Container()
732+
}
646733
}
647734
}
648735
}

private/buf/buflsp/symbol.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,9 @@ func protowireTypeForPredeclared(name predeclared.Name) protowire.Type {
350350
// This helper function expects that imports, tags, and predeclared (builtin) types are
351351
// already handled, since those types currently do not get docs from their comments.
352352
func (s *symbol) getDocsFromComments() string {
353+
if s.def == nil {
354+
return ""
355+
}
353356
var def ast.DeclDef
354357
switch s.kind.(type) {
355358
case *referenceable:

0 commit comments

Comments
 (0)