diff --git a/zcl/json/structure.go b/zcl/json/structure.go index a48f6c6..ec748aa 100644 --- a/zcl/json/structure.go +++ b/zcl/json/structure.go @@ -25,9 +25,40 @@ type expression struct { } func (b *body) Content(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Diagnostics) { - content, _, diags := b.PartialContent(schema) + content, newBody, diags := b.PartialContent(schema) - // TODO: generate errors for the stuff we didn't use in PartialContent + hiddenAttrs := newBody.(*body).hiddenAttrs + + var nameSuggestions []string + for _, attrS := range schema.Attributes { + if _, ok := hiddenAttrs[attrS.Name]; !ok { + // Only suggest an attribute name if we didn't use it already. + nameSuggestions = append(nameSuggestions, attrS.Name) + } + } + for _, blockS := range schema.Blocks { + // Blocks can appear multiple times, so we'll suggest their type + // names regardless of whether they've already been used. + nameSuggestions = append(nameSuggestions, blockS.Type) + } + + for k, attr := range b.obj.Attrs { + if _, ok := hiddenAttrs[k]; !ok { + var fixItHint string + suggestion := nameSuggestion(k, nameSuggestions) + if suggestion != "" { + fixItHint = fmt.Sprintf(" Did you mean %q?", suggestion) + } + + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Extraneous JSON object property", + Detail: fmt.Sprintf("No attribute or block type is named %q.%s", k, fixItHint), + Subject: &attr.NameRange, + Context: attr.Range().Ptr(), + }) + } + } return content, diags } @@ -61,7 +92,6 @@ func (b *body) PartialContent(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Bod Subject: &obj.OpenRange, }) } - usedNames[attrS.Name] = struct{}{} continue } content.Attributes[attrS.Name] = &zcl.Attribute{ diff --git a/zcl/json/structure_test.go b/zcl/json/structure_test.go index f993654..622e5f7 100644 --- a/zcl/json/structure_test.go +++ b/zcl/json/structure_test.go @@ -527,7 +527,7 @@ func TestBodyPartialContent(t *testing.T) { t.Run(fmt.Sprintf("%02d-%s", i, test.src), func(t *testing.T) { file, diags := Parse([]byte(test.src), "test.json") if len(diags) != 0 { - t.Errorf("Parse produced diagnostics: %s", diags) + t.Fatalf("Parse produced diagnostics: %s", diags) } got, _, diags := file.Body.PartialContent(test.schema) if len(diags) != test.diagCount { @@ -543,3 +543,57 @@ func TestBodyPartialContent(t *testing.T) { }) } } + +func TestBodyContent(t *testing.T) { + // We test most of the functionality already in TestBodyPartialContent, so + // this test focuses on the handling of extraneous attributes. + tests := []struct { + src string + schema *zcl.BodySchema + diagCount int + }{ + { + `{"unknown": true}`, + &zcl.BodySchema{}, + 1, + }, + { + `{"unknow": true}`, + &zcl.BodySchema{ + Attributes: []zcl.AttributeSchema{ + { + Name: "unknown", + }, + }, + }, + 1, + }, + { + `{"unknow": true, "unnown": true}`, + &zcl.BodySchema{ + Attributes: []zcl.AttributeSchema{ + { + Name: "unknown", + }, + }, + }, + 2, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%02d-%s", i, test.src), func(t *testing.T) { + file, diags := Parse([]byte(test.src), "test.json") + if len(diags) != 0 { + t.Fatalf("Parse produced diagnostics: %s", diags) + } + _, diags = file.Body.Content(test.schema) + if len(diags) != test.diagCount { + t.Errorf("Wrong number of diagnostics %d; want %d", len(diags), test.diagCount) + for _, diag := range diags { + t.Logf(" - %s", diag) + } + } + }) + } +}