Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hclsyntax: Recovery of outer ObjectConsExpr with incomplete nested ObjectConsExpr #597

Open
radeksimko opened this issue Mar 13, 2023 · 0 comments

Comments

@radeksimko
Copy link
Member

Currently when the configuration contains nested object expressions where the inner one is incomplete, such as

attr = {
  foo = {
    bar
  }
}

this trips up the parser and results in the foo item not being reported at all.

Here's a test to demonstrate the behaviour:

func TestParseConfig_incompleteObject(t *testing.T) {
	tests := []struct {
		input string
		want  *Body
	}{
		{
			`attr = {
  foo = {
    bar
  }
}`,
			&Body{
				Attributes: Attributes{
					"attr": {
						Name: "attr",
						Expr: &ObjectConsExpr{
							Items: []ObjectConsItem{
								{
									KeyExpr: &ObjectConsKeyExpr{
										Wrapped: &ScopeTraversalExpr{
											Traversal: hcl.Traversal{
												hcl.TraverseRoot{
													Name: "foo",
													SrcRange: hcl.Range{
														Start: hcl.Pos{Line: 2, Column: 3, Byte: 11},
														End:   hcl.Pos{Line: 2, Column: 6, Byte: 14},
													},
												},
											},
										},
									},
									ValueExpr: &ObjectConsExpr{
										Items: []ObjectConsItem{},
										SrcRange: hcl.Range{
											Start: hcl.Pos{Line: 2, Column: 9, Byte: 17},
											End:   hcl.Pos{Line: 4, Column: 4, Byte: 30},
										},
										OpenRange: hcl.Range{
											Start: hcl.Pos{Line: 2, Column: 9, Byte: 17},
											End:   hcl.Pos{Line: 2, Column: 9, Byte: 17},
										},
									},
								},
							},
							SrcRange: hcl.Range{
								Start: hcl.Pos{Line: 1, Column: 8, Byte: 7},
								End:   hcl.Pos{Line: 5, Column: 2, Byte: 32},
							},
							OpenRange: hcl.Range{
								Start: hcl.Pos{Line: 1, Column: 8, Byte: 7},
								End:   hcl.Pos{Line: 1, Column: 9, Byte: 8},
							},
						},
						SrcRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
							End:   hcl.Pos{Line: 5, Column: 2, Byte: 32},
						},
						EqualsRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 6, Byte: 5},
							End:   hcl.Pos{Line: 1, Column: 7, Byte: 6},
						},
						NameRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
							End:   hcl.Pos{Line: 1, Column: 5, Byte: 4},
						},
					},
				},
				Blocks: Blocks{},
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 5, Column: 2, Byte: 32},
				},
				EndRange: hcl.Range{
					Start: hcl.Pos{Line: 5, Column: 2, Byte: 32},
					End:   hcl.Pos{Line: 5, Column: 2, Byte: 32},
				},
			},
		},
	}

	opts := cmp.Options{
		cmpopts.IgnoreUnexported(Body{}),
	}
	for i, test := range tests {
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			t.Logf("\n%s", test.input)
			file, _ := ParseConfig([]byte(test.input), "", hcl.InitialPos)

			got := file.Body

			if diff := cmp.Diff(test.want, got, opts); diff != "" {
				t.Errorf(diff)
			}
		})
	}
}

which fails in the following way:

    parser_test.go:3612:   &hclsyntax.Body{
          	Attributes: hclsyntax.Attributes{
          		"attr": &{
          			Name: "attr",
          			Expr: &hclsyntax.ObjectConsExpr{
        - 				Items: []hclsyntax.ObjectConsItem{
        - 					{
        - 						KeyExpr:   &hclsyntax.ObjectConsKeyExpr{Wrapped: &hclsyntax.ScopeTraversalExpr{}},
        - 						ValueExpr: &hclsyntax.ObjectConsExpr{},
        - 					},
        - 				},
        + 				Items:     nil,
          				SrcRange:  hcl.Range{Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, End: hcl.Pos{Line: 5, Column: 2, Byte: 32}},
          				OpenRange: hcl.Range{Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, End: hcl.Pos{Line: 1, Column: 9, Byte: 8}},
          			},
          			SrcRange:    hcl.Range{Start: hcl.Pos{Line: 1, Column: 1}, End: hcl.Pos{Line: 5, Column: 2, Byte: 32}},
          			NameRange:   hcl.Range{Start: hcl.Pos{Line: 1, Column: 1}, End: hcl.Pos{Line: 1, Column: 5, Byte: 4}},
          			EqualsRange: hcl.Range{Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, End: hcl.Pos{Line: 1, Column: 7, Byte: 6}},
          		},
          	},
          	Blocks: hclsyntax.Blocks{},
          	... // 2 ignored and 2 identical fields
          }

I expect the inner (incomplete) object to not be parsed, but it would still be very helpful to have the outer object(s) parsed correctly.

The reason this is important is because it would enable "partial" completion in an IDE, where bar may be exact attribute name, or beginning of an attribute name. We can recover the 3 bytes of bar, but it makes it very difficult/impossible to understand where we are in the config if the remaining outer object isn't returned by the parser.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant