Skip to content

Commit 7b86d68

Browse files
committed
eval: tolerate syntax errors
We must tolerate syntax errors when loading an environment. Returning a nil `EnvironmentDecl` prevents us from being able to analyze the portions of the environment that were successfully parsed. This interferes with analysis of environments that are only partially erroneous. One important scenario this covers is autocompletion in the face of syntax errors, which is common while editing an environment.
1 parent b783e3e commit 7b86d68

File tree

13 files changed

+13736
-18
lines changed

13 files changed

+13736
-18
lines changed

CHANGELOG_PENDING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@
99

1010
### Bug Fixes
1111

12+
- Environment declarations are now returned even in the face of syntax errors.
13+
1214
### Breaking changes

ast/expr.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,7 @@ func parseFromBase64(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr
892892
func parseSecret(node *syntax.ObjectNode, name *StringExpr, value Expr) (Expr, syntax.Diagnostics) {
893893
if arg, ok := value.(*ObjectExpr); ok && len(arg.Entries) == 1 {
894894
kvp := arg.Entries[0]
895-
if kvp.Key.Value == "ciphertext" {
895+
if kvp.Key.GetValue() == "ciphertext" {
896896
if str, ok := kvp.Value.(*StringExpr); ok {
897897
return CiphertextSyntax(node, name, arg, str), nil
898898
}

eval/eval.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,6 @@ func LoadYAMLBytes(filename string, source []byte) (*ast.EnvironmentDecl, syntax
7070

7171
t, tdiags := ast.ParseEnvironment(source, syn)
7272
diags.Extend(tdiags...)
73-
if tdiags.HasErrors() {
74-
return nil, diags, nil
75-
}
76-
7773
return t, diags, nil
7874
}
7975

@@ -391,11 +387,14 @@ func declare[Expr exprNode](e *evalContext, path string, x Expr, base *value) *e
391387
case *ast.ObjectExpr:
392388
properties := make(map[string]*expr, len(x.Entries))
393389
for _, entry := range x.Entries {
394-
k := entry.Key.Value
395-
if _, ok := properties[k]; ok {
396-
e.errorf(entry.Key, "duplicate key %q", k)
397-
} else {
398-
properties[k] = declare(e, util.JoinKey(path, k), entry.Value, base.property(entry.Key, k))
390+
// Possible during parse errors. We elide properties with nil keys.
391+
if entry.Key != nil {
392+
k := entry.Key.Value
393+
if _, ok := properties[k]; ok {
394+
e.errorf(entry.Key, "duplicate key %q", k)
395+
} else {
396+
properties[k] = declare(e, util.JoinKey(path, k), entry.Value, base.property(entry.Key, k))
397+
}
399398
}
400399
}
401400
repr := &objectExpr{node: x, properties: properties}

eval/eval_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ type testEnvironments struct {
252252
}
253253

254254
func (e *testEnvironments) LoadEnvironment(ctx context.Context, name string) ([]byte, Decrypter, error) {
255+
if e.root == "" {
256+
return nil, nil, os.ErrNotExist
257+
}
258+
255259
bytes, err := os.ReadFile(filepath.Join(e.root, name+".yaml"))
256260
if err != nil {
257261
return nil, nil, err
@@ -600,3 +604,37 @@ func BenchmarkEvalEnvLoad(b *testing.B) {
600604
func BenchmarkEvalAll(b *testing.B) {
601605
benchmarkEval(b, 10*time.Millisecond, 10*time.Millisecond)
602606
}
607+
608+
// TestSyntaxErrorCheck provides additional insurance that we are able to parse and analyze environments that contain
609+
// syntax errors. It is important that we are able to provide as much information about an environment as we can so
610+
// that tools that depend on an environment's typed AST or implied schema needs this fix can operate properly, even on
611+
// environment definitions that contain syntax errors. Most notably we need this for autocomplete, but other
612+
// type/schema-based features also benefit (signature help, go-to-def, et cetera).
613+
//
614+
// This is also covered by tests under TestEval, but it can be easy to miss the magnitude of changes to those tests.
615+
func TestSyntaxErrorCheck(t *testing.T) {
616+
environmentName := "syntax-error"
617+
envBytes := []byte(`values:
618+
foo:
619+
bar: ${foo.}
620+
`)
621+
622+
env, loadDiags, err := LoadYAMLBytes(environmentName, envBytes)
623+
require.NoError(t, err)
624+
sortEnvironmentDiagnostics(loadDiags)
625+
626+
execContext, err := esc.NewExecContext(map[string]esc.Value{
627+
"pulumi": esc.NewValue(map[string]esc.Value{
628+
"user": esc.NewValue(map[string]esc.Value{
629+
"id": esc.NewValue("USER_123"),
630+
}),
631+
}),
632+
})
633+
assert.NoError(t, err)
634+
635+
check, checkDiags := CheckEnvironment(context.Background(), environmentName, env, rot128{}, testProviders{},
636+
&testEnvironments{}, execContext, false)
637+
638+
assert.True(t, checkDiags.HasErrors())
639+
assert.NotNil(t, check)
640+
}

eval/expr.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,9 @@ func (x *expr) export(environment string) esc.Expr {
300300
case *objectExpr:
301301
ex.KeyRanges = make(map[string]esc.Range, len(repr.node.Entries))
302302
for _, kvp := range repr.node.Entries {
303-
ex.KeyRanges[kvp.Key.Value] = convertRange(kvp.Key.Syntax().Syntax().Range(), environment)
303+
if kvp.Key != nil {
304+
ex.KeyRanges[kvp.Key.Value] = convertRange(kvp.Key.Syntax().Syntax().Range(), environment)
305+
}
304306
}
305307

306308
ex.Object = make(map[string]esc.Expr, len(repr.properties))

0 commit comments

Comments
 (0)