github.com/hashicorp/hcl/v2@v2.20.0/merged.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package hcl
     5  
     6  import (
     7  	"fmt"
     8  )
     9  
    10  // MergeFiles combines the given files to produce a single body that contains
    11  // configuration from all of the given files.
    12  //
    13  // The ordering of the given files decides the order in which contained
    14  // elements will be returned. If any top-level attributes are defined with
    15  // the same name across multiple files, a diagnostic will be produced from
    16  // the Content and PartialContent methods describing this error in a
    17  // user-friendly way.
    18  func MergeFiles(files []*File) Body {
    19  	var bodies []Body
    20  	for _, file := range files {
    21  		bodies = append(bodies, file.Body)
    22  	}
    23  	return MergeBodies(bodies)
    24  }
    25  
    26  // MergeBodies is like MergeFiles except it deals directly with bodies, rather
    27  // than with entire files.
    28  func MergeBodies(bodies []Body) Body {
    29  	if len(bodies) == 0 {
    30  		// Swap out for our singleton empty body, to reduce the number of
    31  		// empty slices we have hanging around.
    32  		return emptyBody
    33  	}
    34  
    35  	// If any of the given bodies are already merged bodies, we'll unpack
    36  	// to flatten to a single mergedBodies, since that's conceptually simpler.
    37  	// This also, as a side-effect, eliminates any empty bodies, since
    38  	// empties are merged bodies with no inner bodies.
    39  	var newLen int
    40  	var flatten bool
    41  	for _, body := range bodies {
    42  		if children, merged := body.(mergedBodies); merged {
    43  			newLen += len(children)
    44  			flatten = true
    45  		} else {
    46  			newLen++
    47  		}
    48  	}
    49  
    50  	if !flatten { // not just newLen == len, because we might have mergedBodies with single bodies inside
    51  		return mergedBodies(bodies)
    52  	}
    53  
    54  	if newLen == 0 {
    55  		// Don't allocate a new empty when we already have one
    56  		return emptyBody
    57  	}
    58  
    59  	new := make([]Body, 0, newLen)
    60  	for _, body := range bodies {
    61  		if children, merged := body.(mergedBodies); merged {
    62  			new = append(new, children...)
    63  		} else {
    64  			new = append(new, body)
    65  		}
    66  	}
    67  	return mergedBodies(new)
    68  }
    69  
    70  var emptyBody = mergedBodies([]Body{})
    71  
    72  // EmptyBody returns a body with no content. This body can be used as a
    73  // placeholder when a body is required but no body content is available.
    74  func EmptyBody() Body {
    75  	return emptyBody
    76  }
    77  
    78  type mergedBodies []Body
    79  
    80  // Content returns the content produced by applying the given schema to all
    81  // of the merged bodies and merging the result.
    82  //
    83  // Although required attributes _are_ supported, they should be used sparingly
    84  // with merged bodies since in this case there is no contextual information
    85  // with which to return good diagnostics. Applications working with merged
    86  // bodies may wish to mark all attributes as optional and then check for
    87  // required attributes afterwards, to produce better diagnostics.
    88  func (mb mergedBodies) Content(schema *BodySchema) (*BodyContent, Diagnostics) {
    89  	// the returned body will always be empty in this case, because mergedContent
    90  	// will only ever call Content on the child bodies.
    91  	content, _, diags := mb.mergedContent(schema, false)
    92  	return content, diags
    93  }
    94  
    95  func (mb mergedBodies) PartialContent(schema *BodySchema) (*BodyContent, Body, Diagnostics) {
    96  	return mb.mergedContent(schema, true)
    97  }
    98  
    99  func (mb mergedBodies) JustAttributes() (Attributes, Diagnostics) {
   100  	attrs := make(map[string]*Attribute)
   101  	var diags Diagnostics
   102  
   103  	for _, body := range mb {
   104  		thisAttrs, thisDiags := body.JustAttributes()
   105  
   106  		if len(thisDiags) != 0 {
   107  			diags = append(diags, thisDiags...)
   108  		}
   109  
   110  		if thisAttrs != nil {
   111  			for name, attr := range thisAttrs {
   112  				if existing := attrs[name]; existing != nil {
   113  					diags = diags.Append(&Diagnostic{
   114  						Severity: DiagError,
   115  						Summary:  "Duplicate argument",
   116  						Detail: fmt.Sprintf(
   117  							"Argument %q was already set at %s",
   118  							name, existing.NameRange.String(),
   119  						),
   120  						Subject: &attr.NameRange,
   121  					})
   122  					continue
   123  				}
   124  
   125  				attrs[name] = attr
   126  			}
   127  		}
   128  	}
   129  
   130  	return attrs, diags
   131  }
   132  
   133  func (mb mergedBodies) MissingItemRange() Range {
   134  	if len(mb) == 0 {
   135  		// Nothing useful to return here, so we'll return some garbage.
   136  		return Range{
   137  			Filename: "<empty>",
   138  		}
   139  	}
   140  
   141  	// arbitrarily use the first body's missing item range
   142  	return mb[0].MissingItemRange()
   143  }
   144  
   145  func (mb mergedBodies) mergedContent(schema *BodySchema, partial bool) (*BodyContent, Body, Diagnostics) {
   146  	// We need to produce a new schema with none of the attributes marked as
   147  	// required, since _any one_ of our bodies can contribute an attribute value.
   148  	// We'll separately check that all required attributes are present at
   149  	// the end.
   150  	mergedSchema := &BodySchema{
   151  		Blocks: schema.Blocks,
   152  	}
   153  	for _, attrS := range schema.Attributes {
   154  		mergedAttrS := attrS
   155  		mergedAttrS.Required = false
   156  		mergedSchema.Attributes = append(mergedSchema.Attributes, mergedAttrS)
   157  	}
   158  
   159  	var mergedLeftovers []Body
   160  	content := &BodyContent{
   161  		Attributes: map[string]*Attribute{},
   162  	}
   163  
   164  	var diags Diagnostics
   165  	for _, body := range mb {
   166  		var thisContent *BodyContent
   167  		var thisLeftovers Body
   168  		var thisDiags Diagnostics
   169  
   170  		if partial {
   171  			thisContent, thisLeftovers, thisDiags = body.PartialContent(mergedSchema)
   172  		} else {
   173  			thisContent, thisDiags = body.Content(mergedSchema)
   174  		}
   175  
   176  		if thisLeftovers != nil {
   177  			mergedLeftovers = append(mergedLeftovers, thisLeftovers)
   178  		}
   179  		if len(thisDiags) != 0 {
   180  			diags = append(diags, thisDiags...)
   181  		}
   182  
   183  		if thisContent.Attributes != nil {
   184  			for name, attr := range thisContent.Attributes {
   185  				if existing := content.Attributes[name]; existing != nil {
   186  					diags = diags.Append(&Diagnostic{
   187  						Severity: DiagError,
   188  						Summary:  "Duplicate argument",
   189  						Detail: fmt.Sprintf(
   190  							"Argument %q was already set at %s",
   191  							name, existing.NameRange.String(),
   192  						),
   193  						Subject: &attr.NameRange,
   194  					})
   195  					continue
   196  				}
   197  				content.Attributes[name] = attr
   198  			}
   199  		}
   200  
   201  		if len(thisContent.Blocks) != 0 {
   202  			content.Blocks = append(content.Blocks, thisContent.Blocks...)
   203  		}
   204  	}
   205  
   206  	// Finally, we check for required attributes.
   207  	for _, attrS := range schema.Attributes {
   208  		if !attrS.Required {
   209  			continue
   210  		}
   211  
   212  		if content.Attributes[attrS.Name] == nil {
   213  			// We don't have any context here to produce a good diagnostic,
   214  			// which is why we warn in the Content docstring to minimize the
   215  			// use of required attributes on merged bodies.
   216  			diags = diags.Append(&Diagnostic{
   217  				Severity: DiagError,
   218  				Summary:  "Missing required argument",
   219  				Detail: fmt.Sprintf(
   220  					"The argument %q is required, but was not set.",
   221  					attrS.Name,
   222  				),
   223  			})
   224  		}
   225  	}
   226  
   227  	leftoverBody := MergeBodies(mergedLeftovers)
   228  	return content, leftoverBody, diags
   229  }