internal/langserver/handlers/validate/validate.go (329 lines of code) (raw):

package validate import ( "fmt" "strings" "github.com/Azure/azapi-lsp/internal/azure/types" "github.com/Azure/azapi-lsp/internal/langserver/diagnostics" "github.com/Azure/azapi-lsp/internal/langserver/handlers/tfschema" "github.com/Azure/azapi-lsp/internal/parser" "github.com/Azure/azapi-lsp/internal/utils" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" ) func NewDiagnostics(src []byte, filename string) diagnostics.Diagnostics { diags := diagnostics.NewDiagnostics() _, schemaDiags := ValidateFile(src, filename) diags.EmptyRootDiagnostic() validateDiags := make(map[string]hcl.Diagnostics) validateDiags[filename] = schemaDiags diags.Append("schema validate", validateDiags) return diags } func ValidateFile(src []byte, filename string) (*hcl.File, hcl.Diagnostics) { file, _ := hclsyntax.ParseConfig(src, filename, hcl.InitialPos) if file == nil { return nil, nil } body, isHcl := file.Body.(*hclsyntax.Body) if !isHcl { return nil, nil } diags := make([]*hcl.Diagnostic, 0) for _, block := range body.Blocks { if block.Type == "resource" && len(block.Labels) > 0 && strings.HasPrefix(block.Labels[0], "azapi_") { if diag := ValidateBlock(src, block); diag != nil { diags = append(diags, diag...) } } } return file, diags } func ValidateBlock(src []byte, block *hclsyntax.Block) hcl.Diagnostics { if block == nil { return nil } schemaValidationAttr := parser.AttributeWithName(block, "schema_validation_enabled") if schemaValidationAttr != nil { if enabled := parser.ToLiteralBoolean(schemaValidationAttr.Expr); enabled != nil && !*enabled { return nil } } typeValue := parser.ExtractAzureResourceType(block) if typeValue == nil { return nil } bodyDef := tfschema.BodyDefinitionFromBlock(block) if bodyDef == nil { return nil } var attribute *hclsyntax.Attribute var hclNode *parser.HclNode if bodyAttribute := parser.AttributeWithName(block, "body"); bodyAttribute != nil { attribute = bodyAttribute hclNode = parser.JsonEncodeExpressionToHclNode(src, attribute.Expr) if hclNode == nil { tokens, _ := hclsyntax.LexExpression(src[attribute.Expr.Range().Start.Byte:attribute.Expr.Range().End.Byte], "", attribute.Expr.Range().Start) hclNode = parser.BuildHclNode(tokens) } } if attribute == nil || hclNode == nil { return nil } if dummy, ok := hclNode.Children["dummy"]; ok { dummy.KeyRange = attribute.NameRange if nameAttribute := parser.AttributeWithName(block, "name"); nameAttribute != nil { if dummy.Children == nil { dummy.Children = make(map[string]*parser.HclNode) } dummy.Children["name"] = &parser.HclNode{ Value: parser.ToLiteral(nameAttribute.Expr), Key: "name", KeyRange: nameAttribute.NameRange, ValueRange: nameAttribute.Expr.Range(), } } diags := Validate(dummy, bodyDef.AsTypeBase()) // update resource doesn't need to check on required properties if block.Labels[0] == "azapi_update_resource" { res := hcl.Diagnostics{} for _, diag := range diags { // TODO: don't hardcode here if !strings.HasSuffix(diag.Summary, " is required, but no definition was found") { res = append(res, diag) } } return res } else { return diags } } return nil } func Validate(hclNode *parser.HclNode, typeBase *types.TypeBase) hcl.Diagnostics { if typeBase == nil || hclNode == nil { return nil } diags := make([]*hcl.Diagnostic, 0) switch t := (*typeBase).(type) { case *types.ArrayType: if !hclNode.IsValueArray() || t.ItemType == nil { break } for _, child := range hclNode.Children { diags = append(diags, Validate(child, t.ItemType.Type)...) } case *types.DiscriminatedObjectType: if !hclNode.IsValueMap() { break } // check base properties otherProperties := make(map[string]*parser.HclNode) for key, value := range hclNode.Children { if def, ok := t.BaseProperties[key]; ok { if def.IsReadOnly() { diags = append(diags, newDiagnostic(ErrorShouldNotDefineReadOnly(key), value.KeyRange)) continue } if def.Type != nil { diags = append(diags, Validate(value, def.Type.Type)...) } } else { otherProperties[key] = value } } // check required base properties for key, value := range t.BaseProperties { if value.IsRequired() && hclNode.Children[key] == nil { diags = append(diags, newDiagnostic(ErrorShouldDefine(key), hclNode.KeyRange)) } } // check other properties which should be defined in discriminated objects if _, ok := otherProperties[t.Discriminator]; !ok { diags = append(diags, newDiagnostic(ErrorShouldDefine(t.Discriminator), hclNode.KeyRange)) break } discriminator := "" discriminatorRange := hclNode.KeyRange discriminatorProp := otherProperties[t.Discriminator] if discriminatorProp != nil { if discriminatorProp.Value != nil { discriminator = strings.TrimPrefix(strings.TrimSuffix(strings.TrimSpace(*discriminatorProp.Value), `"`), `"`) } discriminatorRange = discriminatorProp.KeyRange } if len(discriminator) != 0 { switch { case t.Elements[discriminator] == nil: options := make([]string, 0) for key := range t.Elements { options = append(options, key) } diags = append(diags, newDiagnostic(ErrorNotMatchAnyValues(t.Discriminator, discriminator, options), discriminatorProp.ValueRange)) case t.Elements[discriminator].Type != nil: other := &parser.HclNode{ Key: hclNode.Key, KeyRange: hclNode.KeyRange, Children: otherProperties, EqualRange: hclNode.EqualRange, ValueRange: hclNode.ValueRange, } diags = append(diags, Validate(other, t.Elements[discriminator].Type)...) } } else { diags = append(diags, newDiagnostic(ErrorMismatch(t.Discriminator, "string", fmt.Sprintf("%T", otherProperties[t.Discriminator])), discriminatorRange)) } case *types.ObjectType: if !hclNode.IsValueMap() { break } // check properties defined in body, but not in schema for key, value := range hclNode.Children { if def, ok := t.Properties[key]; ok { if def.IsReadOnly() { diags = append(diags, newDiagnostic(ErrorShouldNotDefineReadOnly(key), value.KeyRange)) continue } if def.Type != nil { diags = append(diags, Validate(value, def.Type.Type)...) } continue } if t.AdditionalProperties != nil { diags = append(diags, Validate(value, t.AdditionalProperties.Type)...) } else { options := make([]string, 0) for key := range t.Properties { options = append(options, key) } diags = append(diags, newDiagnostic(ErrorShouldNotDefine(key, options), value.KeyRange)) } } // check properties required in schema, but not in body for key, value := range t.Properties { if value.IsRequired() && hclNode.Children[key] == nil { // skip name in body if hclNode.Key == "dummy" && (key == "name" || key == "location") { continue } diags = append(diags, newDiagnostic(ErrorShouldDefine(key), hclNode.KeyRange)) } } case *types.ResourceType: if t.Body != nil { return Validate(hclNode, t.Body.Type) } case *types.ResourceFunctionType: if t.Input != nil { return Validate(hclNode, t.Input.Type) } case *types.AnyType: case *types.BooleanType: case *types.IntegerType: // TODO: validate case *types.StringType: // TODO: validate case *types.StringLiteralType: if hclNode.Value != nil { value := strings.TrimSpace(*hclNode.Value) if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) { value = strings.TrimPrefix(strings.TrimSuffix(value, `"`), `"`) if value != t.Value { diags = append(diags, newDiagnostic(ErrorMismatch(hclNode.Key, t.Value, value), hclNode.ValueRange)) } } } case *types.UnionType: valid := false for _, element := range t.Elements { if element.Type == nil { continue } temp := Validate(hclNode, element.Type) if len(temp) == 0 { valid = true break } } if !valid { options := make([]string, 0) for _, element := range t.Elements { if element.Type != nil { if stringLiteralType, ok := (*element.Type).(*types.StringLiteralType); ok { options = append(options, stringLiteralType.Value) } } } if len(options) == 0 { diags = append(diags, newDiagnostic(ErrorNotMatchAny(hclNode.Key), hclNode.GetRange())) } else { value := "" if hclNode.Value != nil { value = *hclNode.Value } diags = append(diags, newDiagnostic(ErrorNotMatchAnyValues(hclNode.Key, value, options), hclNode.ValueRange)) } } } return diags } func newDiagnostic(summary string, r hcl.Range) *hcl.Diagnostic { return &hcl.Diagnostic{ Summary: summary, Subject: utils.Range(r), Severity: hcl.DiagError, } } func ErrorMismatch(key, expected, actual string) string { return fmt.Sprintf("`%s` is invalid, expect `%s` but got `%s`", strings.TrimPrefix(key, "."), expected, actual) } func ErrorNotMatchAny(key string) string { return fmt.Sprintf("`%s` doesn't match any accepted values", strings.TrimPrefix(key, ".")) } func ErrorNotMatchAnyValues(key string, value string, options []string) string { suggestion := getSuggestion(value, options) return fmt.Sprintf("`%s`'s value `%s` is invalid. The supported values are [%s]. Do you mean `%s`? ", strings.TrimPrefix(key, "."), value, strings.Join(options, ", "), suggestion) } func ErrorShouldNotDefineReadOnly(key string) string { return fmt.Sprintf("`%s` is not expected here, it's read only", strings.TrimPrefix(key, ".")) } func ErrorShouldNotDefine(key string, options []string) string { suggestion := getSuggestion(key, options) return fmt.Sprintf("`%s` is not expected here. Do you mean `%s`? ", strings.TrimPrefix(key, "."), strings.TrimPrefix(suggestion, ".")) } func ErrorShouldDefine(key string) string { return fmt.Sprintf("`%s` is required, but no definition was found", strings.TrimPrefix(key, ".")) } func getSuggestion(value string, options []string) string { suggestion := "" distance := 1 << 16 for _, option := range options { if dist := editDistance(value, option); dist < distance { distance = dist suggestion = option } } return suggestion } func editDistance(a, b string) int { n, m := len(a), len(b) f := make([][]int, n+1) for i := range f { f[i] = make([]int, m+1, 1<<16) } for i := 1; i <= n; i++ { for j := 1; j <= m; j++ { f[i][j] = 1 << 16 } } for i := 1; i <= n; i++ { for j := 1; j <= m; j++ { if a[i-1] == b[j-1] { f[i][j] = f[i-1][j-1] } if f[i][j] > f[i-1][j]+1 { f[i][j] = f[i-1][j] + 1 } if f[i][j] > f[i][j-1]+1 { f[i][j] = f[i][j-1] + 1 } if f[i][j] > f[i-1][j-1]+1 { f[i][j] = f[i-1][j-1] + 1 } } } return f[n][m] }