Skip to content

Commit

Permalink
ast: ranges for accessors in single-line flow strings (#231)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pgavlin authored Feb 1, 2024
1 parent 77fb708 commit e5378c6
Show file tree
Hide file tree
Showing 29 changed files with 6,383 additions and 308 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@
- Improve property accessor diagnostics.
[#230](https://github.com/pulumi/esc/pull/230)

- Populate source positions for property accessors in single-line flow scalars.
[#231](https://github.com/pulumi/esc/pull/231)

### Bug Fixes
27 changes: 21 additions & 6 deletions ast/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,31 @@ func (x *exprNode) Syntax() syntax.Node {
return x.syntax
}

// ExprError creates an error-level diagnostic associated with the given expression. If the expression is non-nil and
// has an underlying syntax node, the error will cover the underlying textual range.
func ExprError(expr Expr, summary string) *syntax.Diagnostic {
var rng *hcl.Range
func exprPosition(expr Expr) (*hcl.Range, string) {
if expr != nil {
if syntax := expr.Syntax(); syntax != nil {
rng = syntax.Syntax().Range()
return syntax.Syntax().Range(), syntax.Syntax().Path()
}
}
return syntax.Error(rng, summary, expr.Syntax().Syntax().Path())
return nil, ""
}

// ExprError creates an error-level diagnostic associated with the given expression. If the expression is non-nil and
// has an underlying syntax node, the error will cover the underlying textual range.
func ExprError(expr Expr, summary string) *syntax.Diagnostic {
rng, path := exprPosition(expr)
return syntax.Error(rng, summary, path)
}

// AccessorError creates an error-level diagnostic associated with the given expression and accessor. If the accessor
// has range information, the error will cover its textual range. Otherwise, the error will cover the textual range of
// the parent expression.
func AccessorError(parent Expr, accessor PropertyAccessor, summary string) *syntax.Diagnostic {
rng, path := exprPosition(parent)
if r := accessor.Range(); r != nil {
rng = r
}
return syntax.Error(rng, summary, path)
}

// A NullExpr represents a null literal.
Expand Down
9 changes: 5 additions & 4 deletions ast/interpolation.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ func parseInterpolate(node syntax.Node, value string) ([]Interpolation, syntax.D
var parts []Interpolation
var str strings.Builder
var diags syntax.Diagnostics
offset := 0
for len(value) > 0 {
switch {
case strings.HasPrefix(value, "$$"):
str.WriteByte('$')
value = value[2:]
value, offset = value[2:], offset+2
case strings.HasPrefix(value, "${"):
rest, access, accessDiags := parsePropertyAccess(node, value[2:])
end, rest, access, accessDiags := parsePropertyAccess(node, offset+2, value[2:])

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

value = rest
value, offset = rest, end
default:
str.WriteByte(value[0])
value = value[1:]
value, offset = value[1:], offset+1
}
}
if str.Len() != 0 {
Expand Down
60 changes: 47 additions & 13 deletions ast/interpolation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,32 @@ package ast
import (
"testing"

"github.com/hashicorp/hcl/v2"
"github.com/pulumi/esc/syntax"
"github.com/stretchr/testify/assert"
)

type SimpleScalar string

func (s SimpleScalar) Range() *hcl.Range {
return &hcl.Range{
Start: hcl.Pos{},
End: hcl.Pos{Byte: len(s)},
}
}

func (s SimpleScalar) Path() string {
return "test"
}

func (s SimpleScalar) ScalarRange(start, end int) *hcl.Range {
r := s.Range()
return &hcl.Range{
Start: hcl.Pos{Byte: r.Start.Byte + start},
End: hcl.Pos{Byte: r.Start.Byte + end},
}
}

func mkInterp(text string, accessors ...PropertyAccessor) Interpolation {
return Interpolation{
Text: text,
Expand All @@ -35,12 +57,24 @@ func mkAccess(accessors ...PropertyAccessor) *PropertyAccess {
return &PropertyAccess{Accessors: accessors}
}

func mkPropertyName(name string) *PropertyName {
return &PropertyName{Name: name}
func mkPropertyName(name string, start, end int) *PropertyName {
return &PropertyName{
Name: name,
AccessorRange: &hcl.Range{
Start: hcl.Pos{Byte: start},
End: hcl.Pos{Byte: end},
},
}
}

func mkPropertySubscript[T string | int](index T) *PropertySubscript {
return &PropertySubscript{Index: index}
func mkPropertySubscript[T string | int](index T, start, end int) *PropertySubscript {
return &PropertySubscript{
Index: index,
AccessorRange: &hcl.Range{
Start: hcl.Pos{Byte: start},
End: hcl.Pos{Byte: end},
},
}
}

func TestInvalidInterpolations(t *testing.T) {
Expand All @@ -50,51 +84,51 @@ func TestInvalidInterpolations(t *testing.T) {
}{
{
text: "${foo",
parts: []Interpolation{mkInterp("", mkPropertyName("foo"))},
parts: []Interpolation{mkInterp("", mkPropertyName("foo", 2, 5))},
},
{
text: "${foo ",
parts: []Interpolation{
mkInterp("", mkPropertyName("foo")),
mkInterp("", mkPropertyName("foo", 2, 5)),
mkInterp(" "),
},
},
{
text: `${foo} ${["baz} bar`,
parts: []Interpolation{
mkInterp("", mkPropertyName("foo")),
mkInterp(" ", mkPropertySubscript("baz} bar")),
mkInterp("", mkPropertyName("foo", 2, 5)),
mkInterp(" ", mkPropertySubscript("baz} bar", 9, 19)),
},
},
{
text: `missing ${property[} subscript`,
parts: []Interpolation{
mkInterp("missing ", mkPropertyName("property"), mkPropertySubscript("")),
mkInterp("missing ", mkPropertyName("property", 10, 18), mkPropertySubscript("", 18, 19)),
mkInterp(" subscript"),
},
},
{
text: `${[bar].baz}`,
parts: []Interpolation{
mkInterp("", mkPropertySubscript("bar"), mkPropertyName("baz")),
mkInterp("", mkPropertySubscript("bar", 2, 7), mkPropertyName("baz", 7, 11)),
},
},
{
text: `${foo.`,
parts: []Interpolation{
mkInterp("", mkPropertyName("foo"), mkPropertyName("")),
mkInterp("", mkPropertyName("foo", 2, 5), mkPropertyName("", 5, 6)),
},
},
{
text: `${foo[`,
parts: []Interpolation{
mkInterp("", mkPropertyName("foo"), mkPropertySubscript("")),
mkInterp("", mkPropertyName("foo", 2, 5), mkPropertySubscript("", 5, 6)),
},
},
}
for _, c := range cases {
t.Run(c.text, func(t *testing.T) {
node := syntax.String(c.text)
node := syntax.StringSyntax(SimpleScalar(c.text), c.text)
parts, diags := parseInterpolate(node, c.text)
assert.NotEmpty(t, diags)
assert.Equal(t, c.parts, parts)
Expand Down
Loading

0 comments on commit e5378c6

Please sign in to comment.