Skip to content

Commit e5378c6

Browse files
authored
ast: ranges for accessors in single-line flow strings (#231)
These changes add support for reporting ranges for accessors in single-line flow strings (i.e. single-line strings that are not quoted). Multi-line strings are quite a bit more complicated to support. The string we get from the YAML parser has already been processed to remove indentation, fold newlines, etc., so its bytes do not represent the bytes in the original file. We will want to add support for these strings in the future, but doing so is something of an open problem. Fixes #232.
1 parent 77fb708 commit e5378c6

File tree

29 files changed

+6383
-308
lines changed

29 files changed

+6383
-308
lines changed

CHANGELOG_PENDING.md

+3
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@
99
- Improve property accessor diagnostics.
1010
[#230](https://github.com/pulumi/esc/pull/230)
1111

12+
- Populate source positions for property accessors in single-line flow scalars.
13+
[#231](https://github.com/pulumi/esc/pull/231)
14+
1215
### Bug Fixes

ast/expr.go

+21-6
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,31 @@ func (x *exprNode) Syntax() syntax.Node {
5252
return x.syntax
5353
}
5454

55-
// ExprError creates an error-level diagnostic associated with the given expression. If the expression is non-nil and
56-
// has an underlying syntax node, the error will cover the underlying textual range.
57-
func ExprError(expr Expr, summary string) *syntax.Diagnostic {
58-
var rng *hcl.Range
55+
func exprPosition(expr Expr) (*hcl.Range, string) {
5956
if expr != nil {
6057
if syntax := expr.Syntax(); syntax != nil {
61-
rng = syntax.Syntax().Range()
58+
return syntax.Syntax().Range(), syntax.Syntax().Path()
6259
}
6360
}
64-
return syntax.Error(rng, summary, expr.Syntax().Syntax().Path())
61+
return nil, ""
62+
}
63+
64+
// ExprError creates an error-level diagnostic associated with the given expression. If the expression is non-nil and
65+
// has an underlying syntax node, the error will cover the underlying textual range.
66+
func ExprError(expr Expr, summary string) *syntax.Diagnostic {
67+
rng, path := exprPosition(expr)
68+
return syntax.Error(rng, summary, path)
69+
}
70+
71+
// AccessorError creates an error-level diagnostic associated with the given expression and accessor. If the accessor
72+
// has range information, the error will cover its textual range. Otherwise, the error will cover the textual range of
73+
// the parent expression.
74+
func AccessorError(parent Expr, accessor PropertyAccessor, summary string) *syntax.Diagnostic {
75+
rng, path := exprPosition(parent)
76+
if r := accessor.Range(); r != nil {
77+
rng = r
78+
}
79+
return syntax.Error(rng, summary, path)
6580
}
6681

6782
// A NullExpr represents a null literal.

ast/interpolation.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@ func parseInterpolate(node syntax.Node, value string) ([]Interpolation, syntax.D
2929
var parts []Interpolation
3030
var str strings.Builder
3131
var diags syntax.Diagnostics
32+
offset := 0
3233
for len(value) > 0 {
3334
switch {
3435
case strings.HasPrefix(value, "$$"):
3536
str.WriteByte('$')
36-
value = value[2:]
37+
value, offset = value[2:], offset+2
3738
case strings.HasPrefix(value, "${"):
38-
rest, access, accessDiags := parsePropertyAccess(node, value[2:])
39+
end, rest, access, accessDiags := parsePropertyAccess(node, offset+2, value[2:])
3940

4041
diags.Extend(accessDiags...)
4142
parts = append(parts, Interpolation{
@@ -44,10 +45,10 @@ func parseInterpolate(node syntax.Node, value string) ([]Interpolation, syntax.D
4445
})
4546
str.Reset()
4647

47-
value = rest
48+
value, offset = rest, end
4849
default:
4950
str.WriteByte(value[0])
50-
value = value[1:]
51+
value, offset = value[1:], offset+1
5152
}
5253
}
5354
if str.Len() != 0 {

ast/interpolation_test.go

+47-13
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,32 @@ package ast
1717
import (
1818
"testing"
1919

20+
"github.com/hashicorp/hcl/v2"
2021
"github.com/pulumi/esc/syntax"
2122
"github.com/stretchr/testify/assert"
2223
)
2324

25+
type SimpleScalar string
26+
27+
func (s SimpleScalar) Range() *hcl.Range {
28+
return &hcl.Range{
29+
Start: hcl.Pos{},
30+
End: hcl.Pos{Byte: len(s)},
31+
}
32+
}
33+
34+
func (s SimpleScalar) Path() string {
35+
return "test"
36+
}
37+
38+
func (s SimpleScalar) ScalarRange(start, end int) *hcl.Range {
39+
r := s.Range()
40+
return &hcl.Range{
41+
Start: hcl.Pos{Byte: r.Start.Byte + start},
42+
End: hcl.Pos{Byte: r.Start.Byte + end},
43+
}
44+
}
45+
2446
func mkInterp(text string, accessors ...PropertyAccessor) Interpolation {
2547
return Interpolation{
2648
Text: text,
@@ -35,12 +57,24 @@ func mkAccess(accessors ...PropertyAccessor) *PropertyAccess {
3557
return &PropertyAccess{Accessors: accessors}
3658
}
3759

38-
func mkPropertyName(name string) *PropertyName {
39-
return &PropertyName{Name: name}
60+
func mkPropertyName(name string, start, end int) *PropertyName {
61+
return &PropertyName{
62+
Name: name,
63+
AccessorRange: &hcl.Range{
64+
Start: hcl.Pos{Byte: start},
65+
End: hcl.Pos{Byte: end},
66+
},
67+
}
4068
}
4169

42-
func mkPropertySubscript[T string | int](index T) *PropertySubscript {
43-
return &PropertySubscript{Index: index}
70+
func mkPropertySubscript[T string | int](index T, start, end int) *PropertySubscript {
71+
return &PropertySubscript{
72+
Index: index,
73+
AccessorRange: &hcl.Range{
74+
Start: hcl.Pos{Byte: start},
75+
End: hcl.Pos{Byte: end},
76+
},
77+
}
4478
}
4579

4680
func TestInvalidInterpolations(t *testing.T) {
@@ -50,51 +84,51 @@ func TestInvalidInterpolations(t *testing.T) {
5084
}{
5185
{
5286
text: "${foo",
53-
parts: []Interpolation{mkInterp("", mkPropertyName("foo"))},
87+
parts: []Interpolation{mkInterp("", mkPropertyName("foo", 2, 5))},
5488
},
5589
{
5690
text: "${foo ",
5791
parts: []Interpolation{
58-
mkInterp("", mkPropertyName("foo")),
92+
mkInterp("", mkPropertyName("foo", 2, 5)),
5993
mkInterp(" "),
6094
},
6195
},
6296
{
6397
text: `${foo} ${["baz} bar`,
6498
parts: []Interpolation{
65-
mkInterp("", mkPropertyName("foo")),
66-
mkInterp(" ", mkPropertySubscript("baz} bar")),
99+
mkInterp("", mkPropertyName("foo", 2, 5)),
100+
mkInterp(" ", mkPropertySubscript("baz} bar", 9, 19)),
67101
},
68102
},
69103
{
70104
text: `missing ${property[} subscript`,
71105
parts: []Interpolation{
72-
mkInterp("missing ", mkPropertyName("property"), mkPropertySubscript("")),
106+
mkInterp("missing ", mkPropertyName("property", 10, 18), mkPropertySubscript("", 18, 19)),
73107
mkInterp(" subscript"),
74108
},
75109
},
76110
{
77111
text: `${[bar].baz}`,
78112
parts: []Interpolation{
79-
mkInterp("", mkPropertySubscript("bar"), mkPropertyName("baz")),
113+
mkInterp("", mkPropertySubscript("bar", 2, 7), mkPropertyName("baz", 7, 11)),
80114
},
81115
},
82116
{
83117
text: `${foo.`,
84118
parts: []Interpolation{
85-
mkInterp("", mkPropertyName("foo"), mkPropertyName("")),
119+
mkInterp("", mkPropertyName("foo", 2, 5), mkPropertyName("", 5, 6)),
86120
},
87121
},
88122
{
89123
text: `${foo[`,
90124
parts: []Interpolation{
91-
mkInterp("", mkPropertyName("foo"), mkPropertySubscript("")),
125+
mkInterp("", mkPropertyName("foo", 2, 5), mkPropertySubscript("", 5, 6)),
92126
},
93127
},
94128
}
95129
for _, c := range cases {
96130
t.Run(c.text, func(t *testing.T) {
97-
node := syntax.String(c.text)
131+
node := syntax.StringSyntax(SimpleScalar(c.text), c.text)
98132
parts, diags := parseInterpolate(node, c.text)
99133
assert.NotEmpty(t, diags)
100134
assert.Equal(t, c.parts, parts)

0 commit comments

Comments
 (0)