rules/nested_block.go (261 lines of code) (raw):

package rules import ( "fmt" "github.com/hashicorp/go-multierror" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/hclwrite" "math" "sort" "strings" ) // NestedBlock is a wrapper of the nested block type NestedBlock struct { File *hcl.File Block *hclsyntax.Block Name string SortField string Range hcl.Range HeadMetaArgs *HeadMetaArgs RequiredArgs *Args OptionalArgs *Args RequiredNestedBlocks *NestedBlocks OptionalNestedBlocks *NestedBlocks ParentBlockNames []string emit func(block Block) error } // CheckBlock checks the nestedBlock recursively to find the block not in order, // and invoke the emit function on that block func (b *NestedBlock) CheckBlock() error { if !b.CheckOrder() { return b.emit(b) } var err error for _, nb := range b.nestedBlocks() { if subErr := nb.CheckBlock(); subErr != nil { err = multierror.Append(err, subErr) } } return err } // DefRange gets the definition range of the nested block func (b *NestedBlock) DefRange() hcl.Range { return b.Block.DefRange() } // CheckOrder checks whether the nestedBlock is sorted func (b *NestedBlock) CheckOrder() bool { return b.checkSubSectionOrder() && b.checkGap() } // ToString prints the sorted block func (b *NestedBlock) ToString() string { headMeta := toString(b.HeadMetaArgs) args := toString(b.RequiredArgs, b.OptionalArgs) nb := toString(b.RequiredNestedBlocks, b.OptionalNestedBlocks) var codes []string for _, c := range []string{headMeta, args, nb} { if c != "" { codes = append(codes, c) } } code := strings.Join(codes, "\n\n") blockHead := string(b.Block.DefRange().SliceBytes(b.File.Bytes)) if strings.TrimSpace(code) == "" { code = fmt.Sprintf("%s {}", blockHead) } else { code = fmt.Sprintf("%s {\n%s\n}", blockHead, code) } return string(hclwrite.Format([]byte(code))) } // NestedBlocks is the collection of nestedBlocks with the same type type NestedBlocks struct { Blocks []*NestedBlock Range *hcl.Range } // CheckOrder checks whether this type of nestedBlocks are sorted func (b *NestedBlocks) CheckOrder() bool { if b == nil { return true } var sortField *string for _, nb := range b.Blocks { if sortField != nil && *sortField > nb.SortField { return false } sortField = &nb.SortField } return true } // ToString prints this type of nestedBlocks in order func (b *NestedBlocks) ToString() string { if b == nil { return "" } sortedBlocks := make([]*NestedBlock, len(b.Blocks)) copy(sortedBlocks, b.Blocks) sort.Slice(sortedBlocks, func(i, j int) bool { return sortedBlocks[i].SortField < sortedBlocks[j].SortField }) var lines []string for _, nb := range sortedBlocks { lines = append(lines, nb.ToString()) } return string(hclwrite.Format([]byte(strings.Join(lines, "\n")))) } // GetRange returns the entire range of this type of nestedBlocks func (b *NestedBlocks) GetRange() *hcl.Range { if b == nil { return nil } return b.Range } func (b *NestedBlocks) add(arg *NestedBlock) { b.Blocks = append(b.Blocks, arg) if b.Range == nil { b.Range = &hcl.Range{ Filename: arg.Range.Filename, Start: hcl.Pos{Line: math.MaxInt}, End: hcl.Pos{Line: -1}, } } if b.Range.Start.Line > arg.Range.Start.Line { b.Range.Start = arg.Range.Start } if b.Range.End.Line < arg.Range.End.Line { b.Range.End = arg.Range.End } } func (b *NestedBlock) nestedBlocks() []*NestedBlock { var nbs []*NestedBlock for _, subNbs := range []*NestedBlocks{b.RequiredNestedBlocks, b.OptionalNestedBlocks} { if subNbs != nil { nbs = append(nbs, subNbs.Blocks...) } } return nbs } func (b *NestedBlock) buildAttributes(attributes hclsyntax.Attributes) { argSchemas := queryBlockSchema(b.ParentBlockNames) attrs := attributesByLines(attributes) for _, attr := range attrs { attrName := attr.Name arg := buildAttrArg(attr, b.File) if IsHeadMeta(attrName) { b.addHeadMeta(arg) continue } if argSchemas == nil { b.addOptionalAttr(arg) continue } attrSchema, isAzAttr := argSchemas.Attributes[attrName] if isAzAttr && attrSchema.Required { b.addRequiredAttr(arg) } else { b.addOptionalAttr(arg) } } } func (b *NestedBlock) buildNestedBlocks(nestedBlock hclsyntax.Blocks) { for _, nb := range nestedBlock { b.buildNestedBlock(nb) } } func (b *NestedBlock) buildNestedBlock(nestedBlock *hclsyntax.Block) { var nestedBlockName, sortField string switch nestedBlock.Type { case "dynamic": nestedBlockName = nestedBlock.Labels[0] sortField = strings.Join(nestedBlock.Labels, "") default: nestedBlockName = nestedBlock.Type sortField = nestedBlock.Type } var parentBlockNames []string if nestedBlockName == "content" && b.Block.Type == "dynamic" { parentBlockNames = b.ParentBlockNames } else { parentBlockNames = append(b.ParentBlockNames, nestedBlockName) } nb := &NestedBlock{ Name: nestedBlockName, SortField: sortField, Range: nestedBlock.Range(), Block: nestedBlock, ParentBlockNames: parentBlockNames, File: b.File, emit: b.emit, } nb.buildAttributes(nestedBlock.Body.Attributes) nb.buildNestedBlocks(nestedBlock.Body.Blocks) blockSchema := queryBlockSchema(b.ParentBlockNames) if metaArgOrUnknownBlock(blockSchema) { b.addOptionalNestedBlock(nb) return } nbSchema, ok := blockSchema.NestedBlocks[nb.Name] if ok && nbSchema.MinItems > 0 { b.addRequiredNestedBlock(nb) } else { b.addOptionalNestedBlock(nb) } } func (b *NestedBlock) addHeadMeta(arg *Arg) { if b.HeadMetaArgs == nil { b.HeadMetaArgs = &HeadMetaArgs{} } b.HeadMetaArgs.add(arg) } func (b *NestedBlock) addRequiredAttr(arg *Arg) { if b.RequiredArgs == nil { b.RequiredArgs = &Args{} } b.RequiredArgs.add(arg) } func (b *NestedBlock) addOptionalAttr(arg *Arg) { if b.OptionalArgs == nil { b.OptionalArgs = &Args{} } b.OptionalArgs.add(arg) } func (b *NestedBlock) addRequiredNestedBlock(nb *NestedBlock) { if b.RequiredNestedBlocks == nil { b.RequiredNestedBlocks = &NestedBlocks{} } b.RequiredNestedBlocks.add(nb) } func (b *NestedBlock) addOptionalNestedBlock(nb *NestedBlock) { if b.OptionalNestedBlocks == nil { b.OptionalNestedBlocks = &NestedBlocks{} } b.OptionalNestedBlocks.add(nb) } func (b *NestedBlock) checkSubSectionOrder() bool { sections := []Section{ b.HeadMetaArgs, b.RequiredArgs, b.OptionalArgs, b.RequiredNestedBlocks, b.OptionalNestedBlocks, } lastEndLine := -1 for _, s := range sections { if !s.CheckOrder() { return false } r := s.GetRange() if r == nil { continue } if r.Start.Line <= lastEndLine { return false } lastEndLine = r.End.Line } return true } func (b *NestedBlock) checkGap() bool { headMetaRange := mergeRange(b.HeadMetaArgs) argRange := mergeRange(b.RequiredArgs, b.OptionalArgs) nbRange := mergeRange(b.RequiredNestedBlocks, b.OptionalNestedBlocks) lastEndLine := -2 for _, r := range []*hcl.Range{headMetaRange, argRange, nbRange} { if r == nil { continue } if r.Start.Line-lastEndLine < 2 { return false } lastEndLine = r.End.Line } return true }