-
Notifications
You must be signed in to change notification settings - Fork 24
/
expression.go
356 lines (311 loc) · 9.55 KB
/
expression.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package decoder
import (
"context"
"unicode"
"unicode/utf8"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/json"
"github.com/zclconf/go-cty/cty"
)
// Expression represents an expression capable of providing
// various LSP features for given hcl.Expression and schema.Constraint.
type Expression interface {
CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate
HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData
SemanticTokens(ctx context.Context) []lang.SemanticToken
}
type ReferenceOriginsExpression interface {
ReferenceOrigins(ctx context.Context, allowSelfRefs bool) reference.Origins
}
type ReferenceTargetsExpression interface {
ReferenceTargets(ctx context.Context, targetCtx *TargetContext) reference.Targets
}
type CanInferTypeExpression interface {
InferType() (cty.Type, bool)
}
// TargetContext describes context for collecting reference targets
type TargetContext struct {
// FriendlyName is (optional) human-readable name of the expression
// interpreted as reference target.
FriendlyName string
// ScopeId defines scope of a reference to allow for more granular
// filtering in completion and accurate matching, which is especially
// important for type-less reference targets (i.e. AsReference: true).
ScopeId lang.ScopeId
// AsExprType defines whether the value of the attribute
// is addressable as a matching literal type constraint included
// in attribute Expr.
//
// cty.DynamicPseudoType (also known as "any type") will create
// reference of the real type if value is present else cty.DynamicPseudoType.
AsExprType bool
// AsReference defines whether the attribute
// is addressable as a type-less reference
AsReference bool
// ParentAddress represents a resolved "parent" absolute address,
// such as data.aws_instance.foo.attr_name.
// This may be address of the attribute, or implied element/item address
// for complex-type expressions such as object, list, map etc.
ParentAddress lang.Address
// ParentLocalAddress represents a resolved "parent" local address,
// such as self.attr_name.
// This may be address of the attribute, or implied element/item address
// for complex-type expressions such as object, list, map etc.
ParentLocalAddress lang.Address
// TargetableFromRangePtr defines where the target is locally targetable
// from via the ParentLocalAddress.
TargetableFromRangePtr *hcl.Range
// ParentRangePtr represents the range of the parent target being collected
// e.g. whole object/map item
ParentRangePtr *hcl.Range
// ParentDefRangePtr represents the range of the parent target's definition
// e.g. object attribute name or map key
ParentDefRangePtr *hcl.Range
}
func (tctx *TargetContext) Copy() *TargetContext {
if tctx == nil {
return nil
}
newCtx := &TargetContext{
FriendlyName: tctx.FriendlyName,
ScopeId: tctx.ScopeId,
AsExprType: tctx.AsExprType,
AsReference: tctx.AsReference,
ParentAddress: tctx.ParentAddress.Copy(),
}
if tctx.ParentLocalAddress != nil {
newCtx.ParentLocalAddress = tctx.ParentLocalAddress.Copy()
}
if tctx.TargetableFromRangePtr != nil {
newCtx.TargetableFromRangePtr = tctx.TargetableFromRangePtr.Ptr()
}
return newCtx
}
func (d *PathDecoder) newExpression(expr hcl.Expression, cons schema.Constraint) Expression {
return newExpression(d.pathCtx, expr, cons)
}
func newExpression(pathContext *PathContext, expr hcl.Expression, cons schema.Constraint) Expression {
switch c := cons.(type) {
case schema.AnyExpression:
return Any{
expr: expr,
cons: c,
pathCtx: pathContext,
}
case schema.LiteralType:
return LiteralType{
expr: expr,
cons: c,
pathCtx: pathContext,
}
case schema.LiteralValue:
return LiteralValue{
expr: expr,
cons: c,
pathCtx: pathContext,
}
case schema.TypeDeclaration:
return TypeDeclaration{
expr: expr,
cons: c,
pathCtx: pathContext,
}
case schema.Keyword:
return Keyword{
expr: expr,
cons: c,
}
case schema.Reference:
return Reference{
expr: expr,
cons: c,
pathCtx: pathContext,
}
case schema.List:
return List{
expr: expr,
cons: c,
pathCtx: pathContext,
}
case schema.Set:
return Set{
expr: expr,
cons: c,
pathCtx: pathContext,
}
case schema.Tuple:
return Tuple{
expr: expr,
cons: c,
pathCtx: pathContext,
}
case schema.Object:
return Object{
expr: expr,
cons: c,
pathCtx: pathContext,
}
case schema.Map:
return Map{
expr: expr,
cons: c,
pathCtx: pathContext,
}
case schema.OneOf:
return OneOf{
expr: expr,
cons: c,
pathCtx: pathContext,
}
}
return unknownExpression{}
}
// isEmptyExpression returns true if given expression is suspected
// to be empty, e.g. newline after equal sign.
//
// Because upstream HCL parser doesn't always handle incomplete
// configuration gracefully, this may not cover all cases.
func isEmptyExpression(expr hcl.Expression) bool {
l, ok := expr.(*hclsyntax.LiteralValueExpr)
if !ok {
return false
}
if l.Val != cty.DynamicVal {
return false
}
return true
}
// newEmptyExpressionAtPos returns a new "artificial" empty expression
// which can be used during completion inside of another expression
// in an empty space which isn't already represented by empty expression.
//
// For example, new argument after comma in function call,
// or new element in a list or set.
func newEmptyExpressionAtPos(filename string, pos hcl.Pos) hcl.Expression {
return &hclsyntax.LiteralValueExpr{
Val: cty.DynamicVal,
SrcRange: hcl.Range{
Filename: filename,
Start: pos,
End: pos,
},
}
}
// recoverLeftBytes seeks left from given pos in given slice of bytes
// and recovers all bytes up until f matches, including that match.
// This allows recovery of incomplete configuration which is not
// present in the parsed AST during completion.
//
// Zero bytes is returned if no match was found.
func recoverLeftBytes(b []byte, pos hcl.Pos, f func(byteOffset int, r rune) bool) []byte {
firstRune, size := utf8.DecodeLastRune(b[:pos.Byte])
offset := pos.Byte - size
// check for early match
if f(pos.Byte, firstRune) {
return b[offset:pos.Byte]
}
for offset > 0 {
nextRune, size := utf8.DecodeLastRune(b[:offset])
if f(offset, nextRune) {
// record the matched offset
// and include the matched last rune
startByte := offset - size
return b[startByte:pos.Byte]
}
offset -= size
}
return []byte{}
}
// recoverRightBytes seeks right from given pos in given slice of bytes
// and recovers all bytes up until f matches, including that match.
// This allows recovery of incomplete configuration which is not
// present in the parsed AST during completion.
//
// Zero bytes is returned if no match was found.
func recoverRightBytes(b []byte, pos hcl.Pos, f func(byteOffset int, r rune) bool) []byte {
nextRune, size := utf8.DecodeRune(b[pos.Byte:])
offset := pos.Byte + size
// check for early match
if f(pos.Byte, nextRune) {
return b[pos.Byte:offset]
}
for offset < len(b) {
nextRune, size := utf8.DecodeRune(b[offset:])
if f(offset, nextRune) {
// record the matched offset
// and include the matched last rune
endByte := offset + size
return b[pos.Byte:endByte]
}
offset += size
}
return []byte{}
}
// isObjectItemTerminatingRune returns true if the given rune
// is considered a left terminating character for an item
// in hclsyntax.ObjectConsExpr.
func isObjectItemTerminatingRune(r rune) bool {
return r == '\n' || r == ',' || r == '{'
}
// isNamespacedFunctionNameRune returns true if the given run
// is a valid character of a namespaced function name.
// This includes letters, digits, dashes, underscores, and colons.
func isNamespacedFunctionNameRune(r rune) bool {
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' || r == '_' || r == ':'
}
// rawObjectKey extracts raw key (as string) from KeyExpr of
// any hclsyntax.ObjectConsExpr along with the corresponding range
// and boolean indicating whether the extraction was successful.
//
// This accounts for the two common key representations (quoted and unquoted)
// and enables validation, filtering of object attributes and accurate
// calculation of edit range.
//
// It does *not* account for interpolation inside the key,
// such as { (var.key_name) = "foo" }.
func rawObjectKey(expr hcl.Expression) (string, *hcl.Range, bool) {
if json.IsJSONExpression(expr) {
val, diags := expr.Value(&hcl.EvalContext{})
if diags.HasErrors() {
return "", nil, false
}
if val.Type() != cty.String {
return "", nil, false
}
return val.AsString(), expr.Range().Ptr(), true
}
// regardless of what expression it is always wrapped
keyExpr, ok := expr.(*hclsyntax.ObjectConsKeyExpr)
if !ok {
return "", nil, false
}
switch eType := keyExpr.Wrapped.(type) {
// most common "naked" keys
case *hclsyntax.ScopeTraversalExpr:
if len(eType.Traversal) != 1 {
return "", nil, false
}
return eType.Traversal.RootName(), eType.Range().Ptr(), true
// less common quoted keys
case *hclsyntax.TemplateExpr:
if !eType.IsStringLiteral() {
return "", nil, false
}
// string literals imply exactly 1 part
lvExpr, ok := eType.Parts[0].(*hclsyntax.LiteralValueExpr)
if !ok {
return "", nil, false
}
if lvExpr.Val.Type() != cty.String {
return "", nil, false
}
return lvExpr.Val.AsString(), lvExpr.Range().Ptr(), true
}
return "", nil, false
}