Mildwonkey/tfconfig upgrade (#23670)
* deps: bump terraform-config-inspect library * configs: parse `version` in new required_providers block With the latest version of `terraform-config-inspect`, the required_providers attribute can now be a string or an object with attributes "source" and "version". This change allows parsing the version constraint from the new object while ignoring any given source attribute.
This commit is contained in:
parent
94ab6d00ae
commit
18dd1bb4d6
|
@ -947,6 +947,56 @@ func TestInit_rcProviders(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInit_providerSource(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
|
||||||
|
configDirName := "init-required-providers"
|
||||||
|
copy.CopyDir(testFixturePath(configDirName), filepath.Join(td, configDirName))
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
m := Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||||
|
Ui: ui,
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &InitCommand{
|
||||||
|
Meta: m,
|
||||||
|
providerInstaller: &mockProviderInstaller{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// make our plugin paths
|
||||||
|
if err := os.MkdirAll(c.pluginDir(), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(DefaultPluginVendorDir, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add some dummy providers
|
||||||
|
// the auto plugin directory
|
||||||
|
testPath := filepath.Join(c.pluginDir(), "terraform-provider-test_v1.2.3_x4")
|
||||||
|
if err := ioutil.WriteFile(testPath, []byte("test bin"), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// the vendor path
|
||||||
|
sourcePath := filepath.Join(DefaultPluginVendorDir, "terraform-provider-source_v1.2.3_x4")
|
||||||
|
if err := ioutil.WriteFile(sourcePath, []byte("test bin"), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{configDirName}
|
||||||
|
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
if strings.Contains(ui.OutputWriter.String(), "Terraform has initialized, but configuration upgrades may be needed") {
|
||||||
|
t.Fatalf("unexpected \"configuration upgrade\" warning in output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInit_getUpgradePlugins(t *testing.T) {
|
func TestInit_getUpgradePlugins(t *testing.T) {
|
||||||
// Create a temporary working directory that is empty
|
// Create a temporary working directory that is empty
|
||||||
td := tempDir(t)
|
td := tempDir(t)
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
test = "1.2.3"
|
||||||
|
source = {
|
||||||
|
version = "1.2.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package configs
|
package configs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
version "github.com/hashicorp/go-version"
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,13 +20,53 @@ func decodeRequiredProvidersBlock(block *hcl.Block) ([]*ProviderRequirement, hcl
|
||||||
attrs, diags := block.Body.JustAttributes()
|
attrs, diags := block.Body.JustAttributes()
|
||||||
var reqs []*ProviderRequirement
|
var reqs []*ProviderRequirement
|
||||||
for name, attr := range attrs {
|
for name, attr := range attrs {
|
||||||
req, reqDiags := decodeVersionConstraint(attr)
|
expr, err := attr.Expr.Value(nil)
|
||||||
|
if err != nil {
|
||||||
|
diags = append(diags, err...)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case expr.Type().IsPrimitiveType():
|
||||||
|
vc, reqDiags := decodeVersionConstraint(attr)
|
||||||
diags = append(diags, reqDiags...)
|
diags = append(diags, reqDiags...)
|
||||||
if !diags.HasErrors() {
|
|
||||||
reqs = append(reqs, &ProviderRequirement{
|
reqs = append(reqs, &ProviderRequirement{
|
||||||
Name: name,
|
Name: name,
|
||||||
Requirement: req,
|
Requirement: vc,
|
||||||
})
|
})
|
||||||
|
case expr.Type().IsObjectType():
|
||||||
|
if expr.Type().HasAttribute("version") {
|
||||||
|
vc := VersionConstraint{
|
||||||
|
DeclRange: attr.Range,
|
||||||
|
}
|
||||||
|
constraintStr := expr.GetAttr("version").AsString()
|
||||||
|
constraints, err := version.NewConstraint(constraintStr)
|
||||||
|
if err != nil {
|
||||||
|
// NewConstraint doesn't return user-friendly errors, so we'll just
|
||||||
|
// ignore the provided error and produce our own generic one.
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid version constraint",
|
||||||
|
Detail: "This string does not use correct version constraint syntax.",
|
||||||
|
Subject: attr.Expr.Range().Ptr(),
|
||||||
|
})
|
||||||
|
reqs = append(reqs, &ProviderRequirement{Name: name})
|
||||||
|
return reqs, diags
|
||||||
|
}
|
||||||
|
vc.Required = constraints
|
||||||
|
reqs = append(reqs, &ProviderRequirement{Name: name, Requirement: vc})
|
||||||
|
}
|
||||||
|
// No version
|
||||||
|
reqs = append(reqs, &ProviderRequirement{Name: name})
|
||||||
|
default:
|
||||||
|
// should not happen
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid provider_requirements syntax",
|
||||||
|
Detail: "provider_requirements entries must be strings or objects.",
|
||||||
|
Subject: attr.Expr.Range().Ptr(),
|
||||||
|
})
|
||||||
|
reqs = append(reqs, &ProviderRequirement{Name: name})
|
||||||
|
return reqs, diags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reqs, diags
|
return reqs, diags
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -72,7 +72,7 @@ require (
|
||||||
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590
|
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590
|
||||||
github.com/hashicorp/memberlist v0.1.0 // indirect
|
github.com/hashicorp/memberlist v0.1.0 // indirect
|
||||||
github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb // indirect
|
github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb // indirect
|
||||||
github.com/hashicorp/terraform-config-inspect v0.0.0-20190821133035-82a99dc22ef4
|
github.com/hashicorp/terraform-config-inspect v0.0.0-20191212124732-c6ae6269b9d7
|
||||||
github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596
|
github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596
|
||||||
github.com/hashicorp/vault v0.10.4
|
github.com/hashicorp/vault v0.10.4
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af
|
||||||
|
|
23
go.sum
23
go.sum
|
@ -93,7 +93,6 @@ github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbM
|
||||||
github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||||
github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e/go.mod h1:N+BjUcTjSxc2mtRGSCPsat1kze3CUtvJN3/jTXlp29k=
|
|
||||||
github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
|
github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
|
||||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
@ -127,7 +126,6 @@ github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1 h1:r1oACdS2XYiA
|
||||||
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1/go.mod h1:lcy9/2gH1jn/VCLouHA6tOEwLoNVd4GW6zhuKLmHC2Y=
|
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1/go.mod h1:lcy9/2gH1jn/VCLouHA6tOEwLoNVd4GW6zhuKLmHC2Y=
|
||||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
|
||||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
@ -190,7 +188,6 @@ github.com/hashicorp/aws-sdk-go-base v0.4.0 h1:zH9hNUdsS+2G0zJaU85ul8D59BGnZBaKM
|
||||||
github.com/hashicorp/aws-sdk-go-base v0.4.0/go.mod h1:eRhlz3c4nhqxFZJAahJEFL7gh6Jyj5rQmQc7F9eHFyQ=
|
github.com/hashicorp/aws-sdk-go-base v0.4.0/go.mod h1:eRhlz3c4nhqxFZJAahJEFL7gh6Jyj5rQmQc7F9eHFyQ=
|
||||||
github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089 h1:1eDpXAxTh0iPv+1kc9/gfSI2pxRERDsTk/lNGolwHn8=
|
github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089 h1:1eDpXAxTh0iPv+1kc9/gfSI2pxRERDsTk/lNGolwHn8=
|
||||||
github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI=
|
github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI=
|
||||||
github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/go-azure-helpers v0.10.0 h1:KhjDnQhCqEMKlt4yH00MCevJQPJ6LkHFdSveXINO6vE=
|
github.com/hashicorp/go-azure-helpers v0.10.0 h1:KhjDnQhCqEMKlt4yH00MCevJQPJ6LkHFdSveXINO6vE=
|
||||||
|
@ -210,7 +207,6 @@ github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa h1:0n
|
||||||
github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa/go.mod h1:6ij3Z20p+OhOkCSrA0gImAWoHYQRGbnlcuk6XYTiaRw=
|
github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa/go.mod h1:6ij3Z20p+OhOkCSrA0gImAWoHYQRGbnlcuk6XYTiaRw=
|
||||||
github.com/hashicorp/go-msgpack v0.5.4 h1:SFT72YqIkOcLdWJUYcriVX7hbrZpwc/f7h8aW2NUqrA=
|
github.com/hashicorp/go-msgpack v0.5.4 h1:SFT72YqIkOcLdWJUYcriVX7hbrZpwc/f7h8aW2NUqrA=
|
||||||
github.com/hashicorp/go-msgpack v0.5.4/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
github.com/hashicorp/go-msgpack v0.5.4/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||||
github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
|
|
||||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
||||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||||
github.com/hashicorp/go-plugin v1.0.1-0.20190610192547-a1bc61569a26 h1:hRho44SAoNu1CBtn5r8Q9J3rCs4ZverWZ4R+UeeNuWM=
|
github.com/hashicorp/go-plugin v1.0.1-0.20190610192547-a1bc61569a26 h1:hRho44SAoNu1CBtn5r8Q9J3rCs4ZverWZ4R+UeeNuWM=
|
||||||
|
@ -239,26 +235,23 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws=
|
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws=
|
||||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||||
|
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
|
||||||
github.com/hashicorp/hcl/v2 v2.2.0 h1:ZQ1eNLggMfTyFBhV8swxT081mlaRjr4EG85NEjjLB84=
|
github.com/hashicorp/hcl/v2 v2.2.0 h1:ZQ1eNLggMfTyFBhV8swxT081mlaRjr4EG85NEjjLB84=
|
||||||
github.com/hashicorp/hcl/v2 v2.2.0/go.mod h1:MD4q2LOluJ5pRwTVkCXmJOY7ODWDXVXGVB8LY0t7wig=
|
github.com/hashicorp/hcl/v2 v2.2.0/go.mod h1:MD4q2LOluJ5pRwTVkCXmJOY7ODWDXVXGVB8LY0t7wig=
|
||||||
github.com/hashicorp/hcl2 v0.0.0-20190821123243-0c888d1241f6 h1:JImQpEeUQ+0DPFMaWzLA0GdUNPaUlCXLpfiqkSZBUfc=
|
|
||||||
github.com/hashicorp/hcl2 v0.0.0-20190821123243-0c888d1241f6/go.mod h1:Cxv+IJLuBiEhQ7pBYGEuORa0nr4U994pE8mYLuFd7v0=
|
|
||||||
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590 h1:2yzhWGdgQUWZUCNK+AoO35V+HTsgEmcM4J9IkArh7PI=
|
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590 h1:2yzhWGdgQUWZUCNK+AoO35V+HTsgEmcM4J9IkArh7PI=
|
||||||
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE=
|
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE=
|
||||||
github.com/hashicorp/memberlist v0.1.0 h1:qSsCiC0WYD39lbSitKNt40e30uorm2Ss/d4JGU1hzH8=
|
github.com/hashicorp/memberlist v0.1.0 h1:qSsCiC0WYD39lbSitKNt40e30uorm2Ss/d4JGU1hzH8=
|
||||||
github.com/hashicorp/memberlist v0.1.0/go.mod h1:ncdBp14cuox2iFOq3kDiquKU6fqsTBc3W6JvZwjxxsE=
|
github.com/hashicorp/memberlist v0.1.0/go.mod h1:ncdBp14cuox2iFOq3kDiquKU6fqsTBc3W6JvZwjxxsE=
|
||||||
github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb h1:ZbgmOQt8DOg796figP87/EFCVx2v2h9yRvwHF/zceX4=
|
github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb h1:ZbgmOQt8DOg796figP87/EFCVx2v2h9yRvwHF/zceX4=
|
||||||
github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE=
|
github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE=
|
||||||
github.com/hashicorp/terraform-config-inspect v0.0.0-20190821133035-82a99dc22ef4 h1:fTkL0YwjohGyN7AqsDhz6bwcGBpT+xBqi3Qhpw58Juw=
|
github.com/hashicorp/terraform-config-inspect v0.0.0-20191212124732-c6ae6269b9d7 h1:Pc5TCv9mbxFN6UVX0LH6CpQrdTM5YjbVI2w15237Pjk=
|
||||||
github.com/hashicorp/terraform-config-inspect v0.0.0-20190821133035-82a99dc22ef4/go.mod h1:JDmizlhaP5P0rYTTZB0reDMefAiJyfWPEtugV4in1oI=
|
github.com/hashicorp/terraform-config-inspect v0.0.0-20191212124732-c6ae6269b9d7/go.mod h1:p+ivJws3dpqbp1iP84+npOyAmTTOLMgCzrXd3GSdn/A=
|
||||||
github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596 h1:hjyO2JsNZUKT1ym+FAdlBEkGPevazYsmVgIMw7dVELg=
|
github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596 h1:hjyO2JsNZUKT1ym+FAdlBEkGPevazYsmVgIMw7dVELg=
|
||||||
github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg=
|
github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg=
|
||||||
github.com/hashicorp/vault v0.10.4 h1:4x0lHxui/ZRp/B3E0Auv1QNBJpzETqHR2kQD3mHSBJU=
|
github.com/hashicorp/vault v0.10.4 h1:4x0lHxui/ZRp/B3E0Auv1QNBJpzETqHR2kQD3mHSBJU=
|
||||||
github.com/hashicorp/vault v0.10.4/go.mod h1:KfSyffbKxoVyspOdlaGVjIuwLobi07qD1bAbosPMpP0=
|
github.com/hashicorp/vault v0.10.4/go.mod h1:KfSyffbKxoVyspOdlaGVjIuwLobi07qD1bAbosPMpP0=
|
||||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
|
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
|
||||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
|
@ -345,9 +338,6 @@ github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/
|
||||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58 h1:m3CEgv3ah1Rhy82L+c0QG/U3VyY1UsvsIdkh0/rU97Y=
|
github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58 h1:m3CEgv3ah1Rhy82L+c0QG/U3VyY1UsvsIdkh0/rU97Y=
|
||||||
github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk=
|
github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk=
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
|
||||||
|
@ -390,6 +380,7 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
|
||||||
github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M=
|
github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M=
|
||||||
github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
@ -451,7 +442,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
@ -460,7 +450,6 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||||
|
@ -479,7 +468,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
@ -541,11 +529,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
|
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
|
||||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
@ -553,5 +539,4 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
|
|
|
@ -86,7 +86,7 @@ func (c *Config) ProviderDependencies() (*moduledeps.Module, tfdiags.Diagnostics
|
||||||
for name, reqs := range c.Module.RequiredProviders {
|
for name, reqs := range c.Module.RequiredProviders {
|
||||||
inst := moduledeps.ProviderInstance(name)
|
inst := moduledeps.ProviderInstance(name)
|
||||||
var constraints version.Constraints
|
var constraints version.Constraints
|
||||||
for _, reqStr := range reqs {
|
for _, reqStr := range reqs.VersionConstraints {
|
||||||
if reqStr != "" {
|
if reqStr != "" {
|
||||||
constraint, err := version.NewConstraint(reqStr)
|
constraint, err := version.NewConstraint(reqStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -200,7 +200,7 @@ func (i *ProviderInstaller) Get(provider addrs.Provider, req Constraints) (Plugi
|
||||||
}
|
}
|
||||||
|
|
||||||
return PluginMeta{}, diags, errwrap.Wrap(ErrorVersionIncompatible, fmt.Errorf(fmt.Sprintf(
|
return PluginMeta{}, diags, errwrap.Wrap(ErrorVersionIncompatible, fmt.Errorf(fmt.Sprintf(
|
||||||
errMsg, provider, v.String(), tfversion.String(),
|
errMsg, provider.LegacyString(), v.String(), tfversion.String(),
|
||||||
closestVersion.String(), closestVersion.MinorUpgradeConstraintStr(), constraintStr)))
|
closestVersion.String(), closestVersion.MinorUpgradeConstraintStr(), constraintStr)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,353 +0,0 @@
|
||||||
Mozilla Public License, version 2.0
|
|
||||||
|
|
||||||
1. Definitions
|
|
||||||
|
|
||||||
1.1. “Contributor”
|
|
||||||
|
|
||||||
means each individual or legal entity that creates, contributes to the
|
|
||||||
creation of, or owns Covered Software.
|
|
||||||
|
|
||||||
1.2. “Contributor Version”
|
|
||||||
|
|
||||||
means the combination of the Contributions of others (if any) used by a
|
|
||||||
Contributor and that particular Contributor’s Contribution.
|
|
||||||
|
|
||||||
1.3. “Contribution”
|
|
||||||
|
|
||||||
means Covered Software of a particular Contributor.
|
|
||||||
|
|
||||||
1.4. “Covered Software”
|
|
||||||
|
|
||||||
means Source Code Form to which the initial Contributor has attached the
|
|
||||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
|
||||||
Modifications of such Source Code Form, in each case including portions
|
|
||||||
thereof.
|
|
||||||
|
|
||||||
1.5. “Incompatible With Secondary Licenses”
|
|
||||||
means
|
|
||||||
|
|
||||||
a. that the initial Contributor has attached the notice described in
|
|
||||||
Exhibit B to the Covered Software; or
|
|
||||||
|
|
||||||
b. that the Covered Software was made available under the terms of version
|
|
||||||
1.1 or earlier of the License, but not also under the terms of a
|
|
||||||
Secondary License.
|
|
||||||
|
|
||||||
1.6. “Executable Form”
|
|
||||||
|
|
||||||
means any form of the work other than Source Code Form.
|
|
||||||
|
|
||||||
1.7. “Larger Work”
|
|
||||||
|
|
||||||
means a work that combines Covered Software with other material, in a separate
|
|
||||||
file or files, that is not Covered Software.
|
|
||||||
|
|
||||||
1.8. “License”
|
|
||||||
|
|
||||||
means this document.
|
|
||||||
|
|
||||||
1.9. “Licensable”
|
|
||||||
|
|
||||||
means having the right to grant, to the maximum extent possible, whether at the
|
|
||||||
time of the initial grant or subsequently, any and all of the rights conveyed by
|
|
||||||
this License.
|
|
||||||
|
|
||||||
1.10. “Modifications”
|
|
||||||
|
|
||||||
means any of the following:
|
|
||||||
|
|
||||||
a. any file in Source Code Form that results from an addition to, deletion
|
|
||||||
from, or modification of the contents of Covered Software; or
|
|
||||||
|
|
||||||
b. any new file in Source Code Form that contains any Covered Software.
|
|
||||||
|
|
||||||
1.11. “Patent Claims” of a Contributor
|
|
||||||
|
|
||||||
means any patent claim(s), including without limitation, method, process,
|
|
||||||
and apparatus claims, in any patent Licensable by such Contributor that
|
|
||||||
would be infringed, but for the grant of the License, by the making,
|
|
||||||
using, selling, offering for sale, having made, import, or transfer of
|
|
||||||
either its Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
1.12. “Secondary License”
|
|
||||||
|
|
||||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
|
||||||
General Public License, Version 2.1, the GNU Affero General Public
|
|
||||||
License, Version 3.0, or any later versions of those licenses.
|
|
||||||
|
|
||||||
1.13. “Source Code Form”
|
|
||||||
|
|
||||||
means the form of the work preferred for making modifications.
|
|
||||||
|
|
||||||
1.14. “You” (or “Your”)
|
|
||||||
|
|
||||||
means an individual or a legal entity exercising rights under this
|
|
||||||
License. For legal entities, “You” includes any entity that controls, is
|
|
||||||
controlled by, or is under common control with You. For purposes of this
|
|
||||||
definition, “control” means (a) the power, direct or indirect, to cause
|
|
||||||
the direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
|
||||||
outstanding shares or beneficial ownership of such entity.
|
|
||||||
|
|
||||||
|
|
||||||
2. License Grants and Conditions
|
|
||||||
|
|
||||||
2.1. Grants
|
|
||||||
|
|
||||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
|
||||||
non-exclusive license:
|
|
||||||
|
|
||||||
a. under intellectual property rights (other than patent or trademark)
|
|
||||||
Licensable by such Contributor to use, reproduce, make available,
|
|
||||||
modify, display, perform, distribute, and otherwise exploit its
|
|
||||||
Contributions, either on an unmodified basis, with Modifications, or as
|
|
||||||
part of a Larger Work; and
|
|
||||||
|
|
||||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
|
||||||
sale, have made, import, and otherwise transfer either its Contributions
|
|
||||||
or its Contributor Version.
|
|
||||||
|
|
||||||
2.2. Effective Date
|
|
||||||
|
|
||||||
The licenses granted in Section 2.1 with respect to any Contribution become
|
|
||||||
effective for each Contribution on the date the Contributor first distributes
|
|
||||||
such Contribution.
|
|
||||||
|
|
||||||
2.3. Limitations on Grant Scope
|
|
||||||
|
|
||||||
The licenses granted in this Section 2 are the only rights granted under this
|
|
||||||
License. No additional rights or licenses will be implied from the distribution
|
|
||||||
or licensing of Covered Software under this License. Notwithstanding Section
|
|
||||||
2.1(b) above, no patent license is granted by a Contributor:
|
|
||||||
|
|
||||||
a. for any code that a Contributor has removed from Covered Software; or
|
|
||||||
|
|
||||||
b. for infringements caused by: (i) Your and any other third party’s
|
|
||||||
modifications of Covered Software, or (ii) the combination of its
|
|
||||||
Contributions with other software (except as part of its Contributor
|
|
||||||
Version); or
|
|
||||||
|
|
||||||
c. under Patent Claims infringed by Covered Software in the absence of its
|
|
||||||
Contributions.
|
|
||||||
|
|
||||||
This License does not grant any rights in the trademarks, service marks, or
|
|
||||||
logos of any Contributor (except as may be necessary to comply with the
|
|
||||||
notice requirements in Section 3.4).
|
|
||||||
|
|
||||||
2.4. Subsequent Licenses
|
|
||||||
|
|
||||||
No Contributor makes additional grants as a result of Your choice to
|
|
||||||
distribute the Covered Software under a subsequent version of this License
|
|
||||||
(see Section 10.2) or under the terms of a Secondary License (if permitted
|
|
||||||
under the terms of Section 3.3).
|
|
||||||
|
|
||||||
2.5. Representation
|
|
||||||
|
|
||||||
Each Contributor represents that the Contributor believes its Contributions
|
|
||||||
are its original creation(s) or it has sufficient rights to grant the
|
|
||||||
rights to its Contributions conveyed by this License.
|
|
||||||
|
|
||||||
2.6. Fair Use
|
|
||||||
|
|
||||||
This License is not intended to limit any rights You have under applicable
|
|
||||||
copyright doctrines of fair use, fair dealing, or other equivalents.
|
|
||||||
|
|
||||||
2.7. Conditions
|
|
||||||
|
|
||||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
|
||||||
Section 2.1.
|
|
||||||
|
|
||||||
|
|
||||||
3. Responsibilities
|
|
||||||
|
|
||||||
3.1. Distribution of Source Form
|
|
||||||
|
|
||||||
All distribution of Covered Software in Source Code Form, including any
|
|
||||||
Modifications that You create or to which You contribute, must be under the
|
|
||||||
terms of this License. You must inform recipients that the Source Code Form
|
|
||||||
of the Covered Software is governed by the terms of this License, and how
|
|
||||||
they can obtain a copy of this License. You may not attempt to alter or
|
|
||||||
restrict the recipients’ rights in the Source Code Form.
|
|
||||||
|
|
||||||
3.2. Distribution of Executable Form
|
|
||||||
|
|
||||||
If You distribute Covered Software in Executable Form then:
|
|
||||||
|
|
||||||
a. such Covered Software must also be made available in Source Code Form,
|
|
||||||
as described in Section 3.1, and You must inform recipients of the
|
|
||||||
Executable Form how they can obtain a copy of such Source Code Form by
|
|
||||||
reasonable means in a timely manner, at a charge no more than the cost
|
|
||||||
of distribution to the recipient; and
|
|
||||||
|
|
||||||
b. You may distribute such Executable Form under the terms of this License,
|
|
||||||
or sublicense it under different terms, provided that the license for
|
|
||||||
the Executable Form does not attempt to limit or alter the recipients’
|
|
||||||
rights in the Source Code Form under this License.
|
|
||||||
|
|
||||||
3.3. Distribution of a Larger Work
|
|
||||||
|
|
||||||
You may create and distribute a Larger Work under terms of Your choice,
|
|
||||||
provided that You also comply with the requirements of this License for the
|
|
||||||
Covered Software. If the Larger Work is a combination of Covered Software
|
|
||||||
with a work governed by one or more Secondary Licenses, and the Covered
|
|
||||||
Software is not Incompatible With Secondary Licenses, this License permits
|
|
||||||
You to additionally distribute such Covered Software under the terms of
|
|
||||||
such Secondary License(s), so that the recipient of the Larger Work may, at
|
|
||||||
their option, further distribute the Covered Software under the terms of
|
|
||||||
either this License or such Secondary License(s).
|
|
||||||
|
|
||||||
3.4. Notices
|
|
||||||
|
|
||||||
You may not remove or alter the substance of any license notices (including
|
|
||||||
copyright notices, patent notices, disclaimers of warranty, or limitations
|
|
||||||
of liability) contained within the Source Code Form of the Covered
|
|
||||||
Software, except that You may alter any license notices to the extent
|
|
||||||
required to remedy known factual inaccuracies.
|
|
||||||
|
|
||||||
3.5. Application of Additional Terms
|
|
||||||
|
|
||||||
You may choose to offer, and to charge a fee for, warranty, support,
|
|
||||||
indemnity or liability obligations to one or more recipients of Covered
|
|
||||||
Software. However, You may do so only on Your own behalf, and not on behalf
|
|
||||||
of any Contributor. You must make it absolutely clear that any such
|
|
||||||
warranty, support, indemnity, or liability obligation is offered by You
|
|
||||||
alone, and You hereby agree to indemnify every Contributor for any
|
|
||||||
liability incurred by such Contributor as a result of warranty, support,
|
|
||||||
indemnity or liability terms You offer. You may include additional
|
|
||||||
disclaimers of warranty and limitations of liability specific to any
|
|
||||||
jurisdiction.
|
|
||||||
|
|
||||||
4. Inability to Comply Due to Statute or Regulation
|
|
||||||
|
|
||||||
If it is impossible for You to comply with any of the terms of this License
|
|
||||||
with respect to some or all of the Covered Software due to statute, judicial
|
|
||||||
order, or regulation then You must: (a) comply with the terms of this License
|
|
||||||
to the maximum extent possible; and (b) describe the limitations and the code
|
|
||||||
they affect. Such description must be placed in a text file included with all
|
|
||||||
distributions of the Covered Software under this License. Except to the
|
|
||||||
extent prohibited by statute or regulation, such description must be
|
|
||||||
sufficiently detailed for a recipient of ordinary skill to be able to
|
|
||||||
understand it.
|
|
||||||
|
|
||||||
5. Termination
|
|
||||||
|
|
||||||
5.1. The rights granted under this License will terminate automatically if You
|
|
||||||
fail to comply with any of its terms. However, if You become compliant,
|
|
||||||
then the rights granted under this License from a particular Contributor
|
|
||||||
are reinstated (a) provisionally, unless and until such Contributor
|
|
||||||
explicitly and finally terminates Your grants, and (b) on an ongoing basis,
|
|
||||||
if such Contributor fails to notify You of the non-compliance by some
|
|
||||||
reasonable means prior to 60 days after You have come back into compliance.
|
|
||||||
Moreover, Your grants from a particular Contributor are reinstated on an
|
|
||||||
ongoing basis if such Contributor notifies You of the non-compliance by
|
|
||||||
some reasonable means, this is the first time You have received notice of
|
|
||||||
non-compliance with this License from such Contributor, and You become
|
|
||||||
compliant prior to 30 days after Your receipt of the notice.
|
|
||||||
|
|
||||||
5.2. If You initiate litigation against any entity by asserting a patent
|
|
||||||
infringement claim (excluding declaratory judgment actions, counter-claims,
|
|
||||||
and cross-claims) alleging that a Contributor Version directly or
|
|
||||||
indirectly infringes any patent, then the rights granted to You by any and
|
|
||||||
all Contributors for the Covered Software under Section 2.1 of this License
|
|
||||||
shall terminate.
|
|
||||||
|
|
||||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
|
||||||
license agreements (excluding distributors and resellers) which have been
|
|
||||||
validly granted by You or Your distributors under this License prior to
|
|
||||||
termination shall survive termination.
|
|
||||||
|
|
||||||
6. Disclaimer of Warranty
|
|
||||||
|
|
||||||
Covered Software is provided under this License on an “as is” basis, without
|
|
||||||
warranty of any kind, either expressed, implied, or statutory, including,
|
|
||||||
without limitation, warranties that the Covered Software is free of defects,
|
|
||||||
merchantable, fit for a particular purpose or non-infringing. The entire
|
|
||||||
risk as to the quality and performance of the Covered Software is with You.
|
|
||||||
Should any Covered Software prove defective in any respect, You (not any
|
|
||||||
Contributor) assume the cost of any necessary servicing, repair, or
|
|
||||||
correction. This disclaimer of warranty constitutes an essential part of this
|
|
||||||
License. No use of any Covered Software is authorized under this License
|
|
||||||
except under this disclaimer.
|
|
||||||
|
|
||||||
7. Limitation of Liability
|
|
||||||
|
|
||||||
Under no circumstances and under no legal theory, whether tort (including
|
|
||||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
|
||||||
distributes Covered Software as permitted above, be liable to You for any
|
|
||||||
direct, indirect, special, incidental, or consequential damages of any
|
|
||||||
character including, without limitation, damages for lost profits, loss of
|
|
||||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses, even if such party shall have been
|
|
||||||
informed of the possibility of such damages. This limitation of liability
|
|
||||||
shall not apply to liability for death or personal injury resulting from such
|
|
||||||
party’s negligence to the extent applicable law prohibits such limitation.
|
|
||||||
Some jurisdictions do not allow the exclusion or limitation of incidental or
|
|
||||||
consequential damages, so this exclusion and limitation may not apply to You.
|
|
||||||
|
|
||||||
8. Litigation
|
|
||||||
|
|
||||||
Any litigation relating to this License may be brought only in the courts of
|
|
||||||
a jurisdiction where the defendant maintains its principal place of business
|
|
||||||
and such litigation shall be governed by laws of that jurisdiction, without
|
|
||||||
reference to its conflict-of-law provisions. Nothing in this Section shall
|
|
||||||
prevent a party’s ability to bring cross-claims or counter-claims.
|
|
||||||
|
|
||||||
9. Miscellaneous
|
|
||||||
|
|
||||||
This License represents the complete agreement concerning the subject matter
|
|
||||||
hereof. If any provision of this License is held to be unenforceable, such
|
|
||||||
provision shall be reformed only to the extent necessary to make it
|
|
||||||
enforceable. Any law or regulation which provides that the language of a
|
|
||||||
contract shall be construed against the drafter shall not be used to construe
|
|
||||||
this License against a Contributor.
|
|
||||||
|
|
||||||
|
|
||||||
10. Versions of the License
|
|
||||||
|
|
||||||
10.1. New Versions
|
|
||||||
|
|
||||||
Mozilla Foundation is the license steward. Except as provided in Section
|
|
||||||
10.3, no one other than the license steward has the right to modify or
|
|
||||||
publish new versions of this License. Each version will be given a
|
|
||||||
distinguishing version number.
|
|
||||||
|
|
||||||
10.2. Effect of New Versions
|
|
||||||
|
|
||||||
You may distribute the Covered Software under the terms of the version of
|
|
||||||
the License under which You originally received the Covered Software, or
|
|
||||||
under the terms of any subsequent version published by the license
|
|
||||||
steward.
|
|
||||||
|
|
||||||
10.3. Modified Versions
|
|
||||||
|
|
||||||
If you create software not governed by this License, and you want to
|
|
||||||
create a new license for such software, you may create and use a modified
|
|
||||||
version of this License if you rename the license and remove any
|
|
||||||
references to the name of the license steward (except to note that such
|
|
||||||
modified license differs from this License).
|
|
||||||
|
|
||||||
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
|
|
||||||
If You choose to distribute Source Code Form that is Incompatible With
|
|
||||||
Secondary Licenses under the terms of this version of the License, the
|
|
||||||
notice described in Exhibit B of this License must be attached.
|
|
||||||
|
|
||||||
Exhibit A - Source Code Form License Notice
|
|
||||||
|
|
||||||
This Source Code Form is subject to the
|
|
||||||
terms of the Mozilla Public License, v.
|
|
||||||
2.0. If a copy of the MPL was not
|
|
||||||
distributed with this file, You can
|
|
||||||
obtain one at
|
|
||||||
http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular file, then
|
|
||||||
You may include the notice in a location (such as a LICENSE file in a relevant
|
|
||||||
directory) where a recipient would be likely to look for such a notice.
|
|
||||||
|
|
||||||
You may add additional accurate notices of copyright ownership.
|
|
||||||
|
|
||||||
Exhibit B - “Incompatible With Secondary Licenses” Notice
|
|
||||||
|
|
||||||
This Source Code Form is “Incompatible
|
|
||||||
With Secondary Licenses”, as defined by
|
|
||||||
the Mozilla Public License, v. 2.0.
|
|
|
@ -1,304 +0,0 @@
|
||||||
package gohcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/zclconf/go-cty/cty/convert"
|
|
||||||
"github.com/zclconf/go-cty/cty/gocty"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DecodeBody extracts the configuration within the given body into the given
|
|
||||||
// value. This value must be a non-nil pointer to either a struct or
|
|
||||||
// a map, where in the former case the configuration will be decoded using
|
|
||||||
// struct tags and in the latter case only attributes are allowed and their
|
|
||||||
// values are decoded into the map.
|
|
||||||
//
|
|
||||||
// The given EvalContext is used to resolve any variables or functions in
|
|
||||||
// expressions encountered while decoding. This may be nil to require only
|
|
||||||
// constant values, for simple applications that do not support variables or
|
|
||||||
// functions.
|
|
||||||
//
|
|
||||||
// The returned diagnostics should be inspected with its HasErrors method to
|
|
||||||
// determine if the populated value is valid and complete. If error diagnostics
|
|
||||||
// are returned then the given value may have been partially-populated but
|
|
||||||
// may still be accessed by a careful caller for static analysis and editor
|
|
||||||
// integration use-cases.
|
|
||||||
func DecodeBody(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
|
|
||||||
rv := reflect.ValueOf(val)
|
|
||||||
if rv.Kind() != reflect.Ptr {
|
|
||||||
panic(fmt.Sprintf("target value must be a pointer, not %s", rv.Type().String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeBodyToValue(body, ctx, rv.Elem())
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeBodyToValue(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics {
|
|
||||||
et := val.Type()
|
|
||||||
switch et.Kind() {
|
|
||||||
case reflect.Struct:
|
|
||||||
return decodeBodyToStruct(body, ctx, val)
|
|
||||||
case reflect.Map:
|
|
||||||
return decodeBodyToMap(body, ctx, val)
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("target value must be pointer to struct or map, not %s", et.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics {
|
|
||||||
schema, partial := ImpliedBodySchema(val.Interface())
|
|
||||||
|
|
||||||
var content *hcl.BodyContent
|
|
||||||
var leftovers hcl.Body
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
if partial {
|
|
||||||
content, leftovers, diags = body.PartialContent(schema)
|
|
||||||
} else {
|
|
||||||
content, diags = body.Content(schema)
|
|
||||||
}
|
|
||||||
if content == nil {
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := getFieldTags(val.Type())
|
|
||||||
|
|
||||||
if tags.Remain != nil {
|
|
||||||
fieldIdx := *tags.Remain
|
|
||||||
field := val.Type().Field(fieldIdx)
|
|
||||||
fieldV := val.Field(fieldIdx)
|
|
||||||
switch {
|
|
||||||
case bodyType.AssignableTo(field.Type):
|
|
||||||
fieldV.Set(reflect.ValueOf(leftovers))
|
|
||||||
case attrsType.AssignableTo(field.Type):
|
|
||||||
attrs, attrsDiags := leftovers.JustAttributes()
|
|
||||||
if len(attrsDiags) > 0 {
|
|
||||||
diags = append(diags, attrsDiags...)
|
|
||||||
}
|
|
||||||
fieldV.Set(reflect.ValueOf(attrs))
|
|
||||||
default:
|
|
||||||
diags = append(diags, decodeBodyToValue(leftovers, ctx, fieldV)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, fieldIdx := range tags.Attributes {
|
|
||||||
attr := content.Attributes[name]
|
|
||||||
field := val.Type().Field(fieldIdx)
|
|
||||||
fieldV := val.Field(fieldIdx)
|
|
||||||
|
|
||||||
if attr == nil {
|
|
||||||
if !exprType.AssignableTo(field.Type) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// As a special case, if the target is of type hcl.Expression then
|
|
||||||
// we'll assign an actual expression that evalues to a cty null,
|
|
||||||
// so the caller can deal with it within the cty realm rather
|
|
||||||
// than within the Go realm.
|
|
||||||
synthExpr := hcl.StaticExpr(cty.NullVal(cty.DynamicPseudoType), body.MissingItemRange())
|
|
||||||
fieldV.Set(reflect.ValueOf(synthExpr))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case attrType.AssignableTo(field.Type):
|
|
||||||
fieldV.Set(reflect.ValueOf(attr))
|
|
||||||
case exprType.AssignableTo(field.Type):
|
|
||||||
fieldV.Set(reflect.ValueOf(attr.Expr))
|
|
||||||
default:
|
|
||||||
diags = append(diags, DecodeExpression(
|
|
||||||
attr.Expr, ctx, fieldV.Addr().Interface(),
|
|
||||||
)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blocksByType := content.Blocks.ByType()
|
|
||||||
|
|
||||||
for typeName, fieldIdx := range tags.Blocks {
|
|
||||||
blocks := blocksByType[typeName]
|
|
||||||
field := val.Type().Field(fieldIdx)
|
|
||||||
|
|
||||||
ty := field.Type
|
|
||||||
isSlice := false
|
|
||||||
isPtr := false
|
|
||||||
if ty.Kind() == reflect.Slice {
|
|
||||||
isSlice = true
|
|
||||||
ty = ty.Elem()
|
|
||||||
}
|
|
||||||
if ty.Kind() == reflect.Ptr {
|
|
||||||
isPtr = true
|
|
||||||
ty = ty.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(blocks) > 1 && !isSlice {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: fmt.Sprintf("Duplicate %s block", typeName),
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"Only one %s block is allowed. Another was defined at %s.",
|
|
||||||
typeName, blocks[0].DefRange.String(),
|
|
||||||
),
|
|
||||||
Subject: &blocks[1].DefRange,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(blocks) == 0 {
|
|
||||||
if isSlice || isPtr {
|
|
||||||
val.Field(fieldIdx).Set(reflect.Zero(field.Type))
|
|
||||||
} else {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: fmt.Sprintf("Missing %s block", typeName),
|
|
||||||
Detail: fmt.Sprintf("A %s block is required.", typeName),
|
|
||||||
Subject: body.MissingItemRange().Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
|
|
||||||
case isSlice:
|
|
||||||
elemType := ty
|
|
||||||
if isPtr {
|
|
||||||
elemType = reflect.PtrTo(ty)
|
|
||||||
}
|
|
||||||
sli := reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks))
|
|
||||||
|
|
||||||
for i, block := range blocks {
|
|
||||||
if isPtr {
|
|
||||||
v := reflect.New(ty)
|
|
||||||
diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...)
|
|
||||||
sli.Index(i).Set(v)
|
|
||||||
} else {
|
|
||||||
diags = append(diags, decodeBlockToValue(block, ctx, sli.Index(i))...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val.Field(fieldIdx).Set(sli)
|
|
||||||
|
|
||||||
default:
|
|
||||||
block := blocks[0]
|
|
||||||
if isPtr {
|
|
||||||
v := reflect.New(ty)
|
|
||||||
diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...)
|
|
||||||
val.Field(fieldIdx).Set(v)
|
|
||||||
} else {
|
|
||||||
diags = append(diags, decodeBlockToValue(block, ctx, val.Field(fieldIdx))...)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
|
|
||||||
attrs, diags := body.JustAttributes()
|
|
||||||
if attrs == nil {
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
mv := reflect.MakeMap(v.Type())
|
|
||||||
|
|
||||||
for k, attr := range attrs {
|
|
||||||
switch {
|
|
||||||
case attrType.AssignableTo(v.Type().Elem()):
|
|
||||||
mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr))
|
|
||||||
case exprType.AssignableTo(v.Type().Elem()):
|
|
||||||
mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr.Expr))
|
|
||||||
default:
|
|
||||||
ev := reflect.New(v.Type().Elem())
|
|
||||||
diags = append(diags, DecodeExpression(attr.Expr, ctx, ev.Interface())...)
|
|
||||||
mv.SetMapIndex(reflect.ValueOf(k), ev.Elem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v.Set(mv)
|
|
||||||
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
|
|
||||||
ty := v.Type()
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case blockType.AssignableTo(ty):
|
|
||||||
v.Elem().Set(reflect.ValueOf(block))
|
|
||||||
case bodyType.AssignableTo(ty):
|
|
||||||
v.Elem().Set(reflect.ValueOf(block.Body))
|
|
||||||
case attrsType.AssignableTo(ty):
|
|
||||||
attrs, attrsDiags := block.Body.JustAttributes()
|
|
||||||
if len(attrsDiags) > 0 {
|
|
||||||
diags = append(diags, attrsDiags...)
|
|
||||||
}
|
|
||||||
v.Elem().Set(reflect.ValueOf(attrs))
|
|
||||||
default:
|
|
||||||
diags = append(diags, decodeBodyToValue(block.Body, ctx, v)...)
|
|
||||||
|
|
||||||
if len(block.Labels) > 0 {
|
|
||||||
blockTags := getFieldTags(ty)
|
|
||||||
for li, lv := range block.Labels {
|
|
||||||
lfieldIdx := blockTags.Labels[li].FieldIndex
|
|
||||||
v.Field(lfieldIdx).Set(reflect.ValueOf(lv))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeExpression extracts the value of the given expression into the given
|
|
||||||
// value. This value must be something that gocty is able to decode into,
|
|
||||||
// since the final decoding is delegated to that package.
|
|
||||||
//
|
|
||||||
// The given EvalContext is used to resolve any variables or functions in
|
|
||||||
// expressions encountered while decoding. This may be nil to require only
|
|
||||||
// constant values, for simple applications that do not support variables or
|
|
||||||
// functions.
|
|
||||||
//
|
|
||||||
// The returned diagnostics should be inspected with its HasErrors method to
|
|
||||||
// determine if the populated value is valid and complete. If error diagnostics
|
|
||||||
// are returned then the given value may have been partially-populated but
|
|
||||||
// may still be accessed by a careful caller for static analysis and editor
|
|
||||||
// integration use-cases.
|
|
||||||
func DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
|
|
||||||
srcVal, diags := expr.Value(ctx)
|
|
||||||
|
|
||||||
convTy, err := gocty.ImpliedType(val)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("unsuitable DecodeExpression target: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
srcVal, err = convert.Convert(srcVal, convTy)
|
|
||||||
if err != nil {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unsuitable value type",
|
|
||||||
Detail: fmt.Sprintf("Unsuitable value: %s", err.Error()),
|
|
||||||
Subject: expr.StartRange().Ptr(),
|
|
||||||
Context: expr.Range().Ptr(),
|
|
||||||
})
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
err = gocty.FromCtyValue(srcVal, val)
|
|
||||||
if err != nil {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unsuitable value type",
|
|
||||||
Detail: fmt.Sprintf("Unsuitable value: %s", err.Error()),
|
|
||||||
Subject: expr.StartRange().Ptr(),
|
|
||||||
Context: expr.Range().Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return diags
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
// Package gohcl allows decoding HCL configurations into Go data structures.
|
|
||||||
//
|
|
||||||
// It provides a convenient and concise way of describing the schema for
|
|
||||||
// configuration and then accessing the resulting data via native Go
|
|
||||||
// types.
|
|
||||||
//
|
|
||||||
// A struct field tag scheme is used, similar to other decoding and
|
|
||||||
// unmarshalling libraries. The tags are formatted as in the following example:
|
|
||||||
//
|
|
||||||
// ThingType string `hcl:"thing_type,attr"`
|
|
||||||
//
|
|
||||||
// Within each tag there are two comma-separated tokens. The first is the
|
|
||||||
// name of the corresponding construct in configuration, while the second
|
|
||||||
// is a keyword giving the kind of construct expected. The following
|
|
||||||
// kind keywords are supported:
|
|
||||||
//
|
|
||||||
// attr (the default) indicates that the value is to be populated from an attribute
|
|
||||||
// block indicates that the value is to populated from a block
|
|
||||||
// label indicates that the value is to populated from a block label
|
|
||||||
// remain indicates that the value is to be populated from the remaining body after populating other fields
|
|
||||||
//
|
|
||||||
// "attr" fields may either be of type *hcl.Expression, in which case the raw
|
|
||||||
// expression is assigned, or of any type accepted by gocty, in which case
|
|
||||||
// gocty will be used to assign the value to a native Go type.
|
|
||||||
//
|
|
||||||
// "block" fields may be of type *hcl.Block or hcl.Body, in which case the
|
|
||||||
// corresponding raw value is assigned, or may be a struct that recursively
|
|
||||||
// uses the same tags. Block fields may also be slices of any of these types,
|
|
||||||
// in which case multiple blocks of the corresponding type are decoded into
|
|
||||||
// the slice.
|
|
||||||
//
|
|
||||||
// "label" fields are considered only in a struct used as the type of a field
|
|
||||||
// marked as "block", and are used sequentially to capture the labels of
|
|
||||||
// the blocks being decoded. In this case, the name token is used only as
|
|
||||||
// an identifier for the label in diagnostic messages.
|
|
||||||
//
|
|
||||||
// "remain" can be placed on a single field that may be either of type
|
|
||||||
// hcl.Body or hcl.Attributes, in which case any remaining body content is
|
|
||||||
// placed into this field for delayed processing. If no "remain" field is
|
|
||||||
// present then any attributes or blocks not matched by another valid tag
|
|
||||||
// will cause an error diagnostic.
|
|
||||||
//
|
|
||||||
// Only a subset of this tagging/typing vocabulary is supported for the
|
|
||||||
// "Encode" family of functions. See the EncodeIntoBody docs for full details
|
|
||||||
// on the constraints there.
|
|
||||||
//
|
|
||||||
// Broadly-speaking this package deals with two types of error. The first is
|
|
||||||
// errors in the configuration itself, which are returned as diagnostics
|
|
||||||
// written with the configuration author as the target audience. The second
|
|
||||||
// is bugs in the calling program, such as invalid struct tags, which are
|
|
||||||
// surfaced via panics since there can be no useful runtime handling of such
|
|
||||||
// errors and they should certainly not be returned to the user as diagnostics.
|
|
||||||
package gohcl
|
|
|
@ -1,191 +0,0 @@
|
||||||
package gohcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hclwrite"
|
|
||||||
"github.com/zclconf/go-cty/cty/gocty"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EncodeIntoBody replaces the contents of the given hclwrite Body with
|
|
||||||
// attributes and blocks derived from the given value, which must be a
|
|
||||||
// struct value or a pointer to a struct value with the struct tags defined
|
|
||||||
// in this package.
|
|
||||||
//
|
|
||||||
// This function can work only with fully-decoded data. It will ignore any
|
|
||||||
// fields tagged as "remain", any fields that decode attributes into either
|
|
||||||
// hcl.Attribute or hcl.Expression values, and any fields that decode blocks
|
|
||||||
// into hcl.Attributes values. This function does not have enough information
|
|
||||||
// to complete the decoding of these types.
|
|
||||||
//
|
|
||||||
// Any fields tagged as "label" are ignored by this function. Use EncodeAsBlock
|
|
||||||
// to produce a whole hclwrite.Block including block labels.
|
|
||||||
//
|
|
||||||
// As long as a suitable value is given to encode and the destination body
|
|
||||||
// is non-nil, this function will always complete. It will panic in case of
|
|
||||||
// any errors in the calling program, such as passing an inappropriate type
|
|
||||||
// or a nil body.
|
|
||||||
//
|
|
||||||
// The layout of the resulting HCL source is derived from the ordering of
|
|
||||||
// the struct fields, with blank lines around nested blocks of different types.
|
|
||||||
// Fields representing attributes should usually precede those representing
|
|
||||||
// blocks so that the attributes can group togather in the result. For more
|
|
||||||
// control, use the hclwrite API directly.
|
|
||||||
func EncodeIntoBody(val interface{}, dst *hclwrite.Body) {
|
|
||||||
rv := reflect.ValueOf(val)
|
|
||||||
ty := rv.Type()
|
|
||||||
if ty.Kind() == reflect.Ptr {
|
|
||||||
rv = rv.Elem()
|
|
||||||
ty = rv.Type()
|
|
||||||
}
|
|
||||||
if ty.Kind() != reflect.Struct {
|
|
||||||
panic(fmt.Sprintf("value is %s, not struct", ty.Kind()))
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := getFieldTags(ty)
|
|
||||||
populateBody(rv, ty, tags, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeAsBlock creates a new hclwrite.Block populated with the data from
|
|
||||||
// the given value, which must be a struct or pointer to struct with the
|
|
||||||
// struct tags defined in this package.
|
|
||||||
//
|
|
||||||
// If the given struct type has fields tagged with "label" tags then they
|
|
||||||
// will be used in order to annotate the created block with labels.
|
|
||||||
//
|
|
||||||
// This function has the same constraints as EncodeIntoBody and will panic
|
|
||||||
// if they are violated.
|
|
||||||
func EncodeAsBlock(val interface{}, blockType string) *hclwrite.Block {
|
|
||||||
rv := reflect.ValueOf(val)
|
|
||||||
ty := rv.Type()
|
|
||||||
if ty.Kind() == reflect.Ptr {
|
|
||||||
rv = rv.Elem()
|
|
||||||
ty = rv.Type()
|
|
||||||
}
|
|
||||||
if ty.Kind() != reflect.Struct {
|
|
||||||
panic(fmt.Sprintf("value is %s, not struct", ty.Kind()))
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := getFieldTags(ty)
|
|
||||||
labels := make([]string, len(tags.Labels))
|
|
||||||
for i, lf := range tags.Labels {
|
|
||||||
lv := rv.Field(lf.FieldIndex)
|
|
||||||
// We just stringify whatever we find. It should always be a string
|
|
||||||
// but if not then we'll still do something reasonable.
|
|
||||||
labels[i] = fmt.Sprintf("%s", lv.Interface())
|
|
||||||
}
|
|
||||||
|
|
||||||
block := hclwrite.NewBlock(blockType, labels)
|
|
||||||
populateBody(rv, ty, tags, block.Body())
|
|
||||||
return block
|
|
||||||
}
|
|
||||||
|
|
||||||
func populateBody(rv reflect.Value, ty reflect.Type, tags *fieldTags, dst *hclwrite.Body) {
|
|
||||||
nameIdxs := make(map[string]int, len(tags.Attributes)+len(tags.Blocks))
|
|
||||||
namesOrder := make([]string, 0, len(tags.Attributes)+len(tags.Blocks))
|
|
||||||
for n, i := range tags.Attributes {
|
|
||||||
nameIdxs[n] = i
|
|
||||||
namesOrder = append(namesOrder, n)
|
|
||||||
}
|
|
||||||
for n, i := range tags.Blocks {
|
|
||||||
nameIdxs[n] = i
|
|
||||||
namesOrder = append(namesOrder, n)
|
|
||||||
}
|
|
||||||
sort.SliceStable(namesOrder, func(i, j int) bool {
|
|
||||||
ni, nj := namesOrder[i], namesOrder[j]
|
|
||||||
return nameIdxs[ni] < nameIdxs[nj]
|
|
||||||
})
|
|
||||||
|
|
||||||
dst.Clear()
|
|
||||||
|
|
||||||
prevWasBlock := false
|
|
||||||
for _, name := range namesOrder {
|
|
||||||
fieldIdx := nameIdxs[name]
|
|
||||||
field := ty.Field(fieldIdx)
|
|
||||||
fieldTy := field.Type
|
|
||||||
fieldVal := rv.Field(fieldIdx)
|
|
||||||
|
|
||||||
if fieldTy.Kind() == reflect.Ptr {
|
|
||||||
fieldTy = fieldTy.Elem()
|
|
||||||
fieldVal = fieldVal.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, isAttr := tags.Attributes[name]; isAttr {
|
|
||||||
|
|
||||||
if exprType.AssignableTo(fieldTy) || attrType.AssignableTo(fieldTy) {
|
|
||||||
continue // ignore undecoded fields
|
|
||||||
}
|
|
||||||
if !fieldVal.IsValid() {
|
|
||||||
continue // ignore (field value is nil pointer)
|
|
||||||
}
|
|
||||||
if fieldTy.Kind() == reflect.Ptr && fieldVal.IsNil() {
|
|
||||||
continue // ignore
|
|
||||||
}
|
|
||||||
if prevWasBlock {
|
|
||||||
dst.AppendNewline()
|
|
||||||
prevWasBlock = false
|
|
||||||
}
|
|
||||||
|
|
||||||
valTy, err := gocty.ImpliedType(fieldVal.Interface())
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("cannot encode %T as HCL expression: %s", fieldVal.Interface(), err))
|
|
||||||
}
|
|
||||||
|
|
||||||
val, err := gocty.ToCtyValue(fieldVal.Interface(), valTy)
|
|
||||||
if err != nil {
|
|
||||||
// This should never happen, since we should always be able
|
|
||||||
// to decode into the implied type.
|
|
||||||
panic(fmt.Sprintf("failed to encode %T as %#v: %s", fieldVal.Interface(), valTy, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
dst.SetAttributeValue(name, val)
|
|
||||||
|
|
||||||
} else { // must be a block, then
|
|
||||||
elemTy := fieldTy
|
|
||||||
isSeq := false
|
|
||||||
if elemTy.Kind() == reflect.Slice || elemTy.Kind() == reflect.Array {
|
|
||||||
isSeq = true
|
|
||||||
elemTy = elemTy.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
if bodyType.AssignableTo(elemTy) || attrsType.AssignableTo(elemTy) {
|
|
||||||
continue // ignore undecoded fields
|
|
||||||
}
|
|
||||||
prevWasBlock = false
|
|
||||||
|
|
||||||
if isSeq {
|
|
||||||
l := fieldVal.Len()
|
|
||||||
for i := 0; i < l; i++ {
|
|
||||||
elemVal := fieldVal.Index(i)
|
|
||||||
if !elemVal.IsValid() {
|
|
||||||
continue // ignore (elem value is nil pointer)
|
|
||||||
}
|
|
||||||
if elemTy.Kind() == reflect.Ptr && elemVal.IsNil() {
|
|
||||||
continue // ignore
|
|
||||||
}
|
|
||||||
block := EncodeAsBlock(elemVal.Interface(), name)
|
|
||||||
if !prevWasBlock {
|
|
||||||
dst.AppendNewline()
|
|
||||||
prevWasBlock = true
|
|
||||||
}
|
|
||||||
dst.AppendBlock(block)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !fieldVal.IsValid() {
|
|
||||||
continue // ignore (field value is nil pointer)
|
|
||||||
}
|
|
||||||
if elemTy.Kind() == reflect.Ptr && fieldVal.IsNil() {
|
|
||||||
continue // ignore
|
|
||||||
}
|
|
||||||
block := EncodeAsBlock(fieldVal.Interface(), name)
|
|
||||||
if !prevWasBlock {
|
|
||||||
dst.AppendNewline()
|
|
||||||
prevWasBlock = true
|
|
||||||
}
|
|
||||||
dst.AppendBlock(block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,174 +0,0 @@
|
||||||
package gohcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ImpliedBodySchema produces a hcl.BodySchema derived from the type of the
|
|
||||||
// given value, which must be a struct value or a pointer to one. If an
|
|
||||||
// inappropriate value is passed, this function will panic.
|
|
||||||
//
|
|
||||||
// The second return argument indicates whether the given struct includes
|
|
||||||
// a "remain" field, and thus the returned schema is non-exhaustive.
|
|
||||||
//
|
|
||||||
// This uses the tags on the fields of the struct to discover how each
|
|
||||||
// field's value should be expressed within configuration. If an invalid
|
|
||||||
// mapping is attempted, this function will panic.
|
|
||||||
func ImpliedBodySchema(val interface{}) (schema *hcl.BodySchema, partial bool) {
|
|
||||||
ty := reflect.TypeOf(val)
|
|
||||||
|
|
||||||
if ty.Kind() == reflect.Ptr {
|
|
||||||
ty = ty.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
if ty.Kind() != reflect.Struct {
|
|
||||||
panic(fmt.Sprintf("given value must be struct, not %T", val))
|
|
||||||
}
|
|
||||||
|
|
||||||
var attrSchemas []hcl.AttributeSchema
|
|
||||||
var blockSchemas []hcl.BlockHeaderSchema
|
|
||||||
|
|
||||||
tags := getFieldTags(ty)
|
|
||||||
|
|
||||||
attrNames := make([]string, 0, len(tags.Attributes))
|
|
||||||
for n := range tags.Attributes {
|
|
||||||
attrNames = append(attrNames, n)
|
|
||||||
}
|
|
||||||
sort.Strings(attrNames)
|
|
||||||
for _, n := range attrNames {
|
|
||||||
idx := tags.Attributes[n]
|
|
||||||
optional := tags.Optional[n]
|
|
||||||
field := ty.Field(idx)
|
|
||||||
|
|
||||||
var required bool
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case field.Type.AssignableTo(exprType):
|
|
||||||
// If we're decoding to hcl.Expression then absense can be
|
|
||||||
// indicated via a null value, so we don't specify that
|
|
||||||
// the field is required during decoding.
|
|
||||||
required = false
|
|
||||||
case field.Type.Kind() != reflect.Ptr && !optional:
|
|
||||||
required = true
|
|
||||||
default:
|
|
||||||
required = false
|
|
||||||
}
|
|
||||||
|
|
||||||
attrSchemas = append(attrSchemas, hcl.AttributeSchema{
|
|
||||||
Name: n,
|
|
||||||
Required: required,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
blockNames := make([]string, 0, len(tags.Blocks))
|
|
||||||
for n := range tags.Blocks {
|
|
||||||
blockNames = append(blockNames, n)
|
|
||||||
}
|
|
||||||
sort.Strings(blockNames)
|
|
||||||
for _, n := range blockNames {
|
|
||||||
idx := tags.Blocks[n]
|
|
||||||
field := ty.Field(idx)
|
|
||||||
fty := field.Type
|
|
||||||
if fty.Kind() == reflect.Slice {
|
|
||||||
fty = fty.Elem()
|
|
||||||
}
|
|
||||||
if fty.Kind() == reflect.Ptr {
|
|
||||||
fty = fty.Elem()
|
|
||||||
}
|
|
||||||
if fty.Kind() != reflect.Struct {
|
|
||||||
panic(fmt.Sprintf(
|
|
||||||
"hcl 'block' tag kind cannot be applied to %s field %s: struct required", field.Type.String(), field.Name,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
ftags := getFieldTags(fty)
|
|
||||||
var labelNames []string
|
|
||||||
if len(ftags.Labels) > 0 {
|
|
||||||
labelNames = make([]string, len(ftags.Labels))
|
|
||||||
for i, l := range ftags.Labels {
|
|
||||||
labelNames[i] = l.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blockSchemas = append(blockSchemas, hcl.BlockHeaderSchema{
|
|
||||||
Type: n,
|
|
||||||
LabelNames: labelNames,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
partial = tags.Remain != nil
|
|
||||||
schema = &hcl.BodySchema{
|
|
||||||
Attributes: attrSchemas,
|
|
||||||
Blocks: blockSchemas,
|
|
||||||
}
|
|
||||||
return schema, partial
|
|
||||||
}
|
|
||||||
|
|
||||||
type fieldTags struct {
|
|
||||||
Attributes map[string]int
|
|
||||||
Blocks map[string]int
|
|
||||||
Labels []labelField
|
|
||||||
Remain *int
|
|
||||||
Optional map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type labelField struct {
|
|
||||||
FieldIndex int
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFieldTags(ty reflect.Type) *fieldTags {
|
|
||||||
ret := &fieldTags{
|
|
||||||
Attributes: map[string]int{},
|
|
||||||
Blocks: map[string]int{},
|
|
||||||
Optional: map[string]bool{},
|
|
||||||
}
|
|
||||||
|
|
||||||
ct := ty.NumField()
|
|
||||||
for i := 0; i < ct; i++ {
|
|
||||||
field := ty.Field(i)
|
|
||||||
tag := field.Tag.Get("hcl")
|
|
||||||
if tag == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
comma := strings.Index(tag, ",")
|
|
||||||
var name, kind string
|
|
||||||
if comma != -1 {
|
|
||||||
name = tag[:comma]
|
|
||||||
kind = tag[comma+1:]
|
|
||||||
} else {
|
|
||||||
name = tag
|
|
||||||
kind = "attr"
|
|
||||||
}
|
|
||||||
|
|
||||||
switch kind {
|
|
||||||
case "attr":
|
|
||||||
ret.Attributes[name] = i
|
|
||||||
case "block":
|
|
||||||
ret.Blocks[name] = i
|
|
||||||
case "label":
|
|
||||||
ret.Labels = append(ret.Labels, labelField{
|
|
||||||
FieldIndex: i,
|
|
||||||
Name: name,
|
|
||||||
})
|
|
||||||
case "remain":
|
|
||||||
if ret.Remain != nil {
|
|
||||||
panic("only one 'remain' tag is permitted")
|
|
||||||
}
|
|
||||||
idx := i // copy, because this loop will continue assigning to i
|
|
||||||
ret.Remain = &idx
|
|
||||||
case "optional":
|
|
||||||
ret.Attributes[name] = i
|
|
||||||
ret.Optional[name] = true
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("invalid hcl field tag kind %q on %s %q", kind, field.Type.String(), field.Name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package gohcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
var victimExpr hcl.Expression
|
|
||||||
var victimBody hcl.Body
|
|
||||||
|
|
||||||
var exprType = reflect.TypeOf(&victimExpr).Elem()
|
|
||||||
var bodyType = reflect.TypeOf(&victimBody).Elem()
|
|
||||||
var blockType = reflect.TypeOf((*hcl.Block)(nil))
|
|
||||||
var attrType = reflect.TypeOf((*hcl.Attribute)(nil))
|
|
||||||
var attrsType = reflect.TypeOf(hcl.Attributes(nil))
|
|
|
@ -1,143 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DiagnosticSeverity represents the severity of a diagnostic.
|
|
||||||
type DiagnosticSeverity int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DiagInvalid is the invalid zero value of DiagnosticSeverity
|
|
||||||
DiagInvalid DiagnosticSeverity = iota
|
|
||||||
|
|
||||||
// DiagError indicates that the problem reported by a diagnostic prevents
|
|
||||||
// further progress in parsing and/or evaluating the subject.
|
|
||||||
DiagError
|
|
||||||
|
|
||||||
// DiagWarning indicates that the problem reported by a diagnostic warrants
|
|
||||||
// user attention but does not prevent further progress. It is most
|
|
||||||
// commonly used for showing deprecation notices.
|
|
||||||
DiagWarning
|
|
||||||
)
|
|
||||||
|
|
||||||
// Diagnostic represents information to be presented to a user about an
|
|
||||||
// error or anomoly in parsing or evaluating configuration.
|
|
||||||
type Diagnostic struct {
|
|
||||||
Severity DiagnosticSeverity
|
|
||||||
|
|
||||||
// Summary and Detail contain the English-language description of the
|
|
||||||
// problem. Summary is a terse description of the general problem and
|
|
||||||
// detail is a more elaborate, often-multi-sentence description of
|
|
||||||
// the probem and what might be done to solve it.
|
|
||||||
Summary string
|
|
||||||
Detail string
|
|
||||||
|
|
||||||
// Subject and Context are both source ranges relating to the diagnostic.
|
|
||||||
//
|
|
||||||
// Subject is a tight range referring to exactly the construct that
|
|
||||||
// is problematic, while Context is an optional broader range (which should
|
|
||||||
// fully contain Subject) that ought to be shown around Subject when
|
|
||||||
// generating isolated source-code snippets in diagnostic messages.
|
|
||||||
// If Context is nil, the Subject is also the Context.
|
|
||||||
//
|
|
||||||
// Some diagnostics have no source ranges at all. If Context is set then
|
|
||||||
// Subject should always also be set.
|
|
||||||
Subject *Range
|
|
||||||
Context *Range
|
|
||||||
|
|
||||||
// For diagnostics that occur when evaluating an expression, Expression
|
|
||||||
// may refer to that expression and EvalContext may point to the
|
|
||||||
// EvalContext that was active when evaluating it. This may allow for the
|
|
||||||
// inclusion of additional useful information when rendering a diagnostic
|
|
||||||
// message to the user.
|
|
||||||
//
|
|
||||||
// It is not always possible to select a single EvalContext for a
|
|
||||||
// diagnostic, and so in some cases this field may be nil even when an
|
|
||||||
// expression causes a problem.
|
|
||||||
//
|
|
||||||
// EvalContexts form a tree, so the given EvalContext may refer to a parent
|
|
||||||
// which in turn refers to another parent, etc. For a full picture of all
|
|
||||||
// of the active variables and functions the caller must walk up this
|
|
||||||
// chain, preferring definitions that are "closer" to the expression in
|
|
||||||
// case of colliding names.
|
|
||||||
Expression Expression
|
|
||||||
EvalContext *EvalContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Diagnostics is a list of Diagnostic instances.
|
|
||||||
type Diagnostics []*Diagnostic
|
|
||||||
|
|
||||||
// error implementation, so that diagnostics can be returned via APIs
|
|
||||||
// that normally deal in vanilla Go errors.
|
|
||||||
//
|
|
||||||
// This presents only minimal context about the error, for compatibility
|
|
||||||
// with usual expectations about how errors will present as strings.
|
|
||||||
func (d *Diagnostic) Error() string {
|
|
||||||
return fmt.Sprintf("%s: %s; %s", d.Subject, d.Summary, d.Detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
// error implementation, so that sets of diagnostics can be returned via
|
|
||||||
// APIs that normally deal in vanilla Go errors.
|
|
||||||
func (d Diagnostics) Error() string {
|
|
||||||
count := len(d)
|
|
||||||
switch {
|
|
||||||
case count == 0:
|
|
||||||
return "no diagnostics"
|
|
||||||
case count == 1:
|
|
||||||
return d[0].Error()
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%s, and %d other diagnostic(s)", d[0].Error(), count-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append appends a new error to a Diagnostics and return the whole Diagnostics.
|
|
||||||
//
|
|
||||||
// This is provided as a convenience for returning from a function that
|
|
||||||
// collects and then returns a set of diagnostics:
|
|
||||||
//
|
|
||||||
// return nil, diags.Append(&hcl.Diagnostic{ ... })
|
|
||||||
//
|
|
||||||
// Note that this modifies the array underlying the diagnostics slice, so
|
|
||||||
// must be used carefully within a single codepath. It is incorrect (and rude)
|
|
||||||
// to extend a diagnostics created by a different subsystem.
|
|
||||||
func (d Diagnostics) Append(diag *Diagnostic) Diagnostics {
|
|
||||||
return append(d, diag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extend concatenates the given Diagnostics with the receiver and returns
|
|
||||||
// the whole new Diagnostics.
|
|
||||||
//
|
|
||||||
// This is similar to Append but accepts multiple diagnostics to add. It has
|
|
||||||
// all the same caveats and constraints.
|
|
||||||
func (d Diagnostics) Extend(diags Diagnostics) Diagnostics {
|
|
||||||
return append(d, diags...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasErrors returns true if the receiver contains any diagnostics of
|
|
||||||
// severity DiagError.
|
|
||||||
func (d Diagnostics) HasErrors() bool {
|
|
||||||
for _, diag := range d {
|
|
||||||
if diag.Severity == DiagError {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Diagnostics) Errs() []error {
|
|
||||||
var errs []error
|
|
||||||
for _, diag := range d {
|
|
||||||
if diag.Severity == DiagError {
|
|
||||||
errs = append(errs, diag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
// A DiagnosticWriter emits diagnostics somehow.
|
|
||||||
type DiagnosticWriter interface {
|
|
||||||
WriteDiagnostic(*Diagnostic) error
|
|
||||||
WriteDiagnostics(Diagnostics) error
|
|
||||||
}
|
|
|
@ -1,311 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
wordwrap "github.com/mitchellh/go-wordwrap"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
|
||||||
|
|
||||||
type diagnosticTextWriter struct {
|
|
||||||
files map[string]*File
|
|
||||||
wr io.Writer
|
|
||||||
width uint
|
|
||||||
color bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDiagnosticTextWriter creates a DiagnosticWriter that writes diagnostics
|
|
||||||
// to the given writer as formatted text.
|
|
||||||
//
|
|
||||||
// It is designed to produce text appropriate to print in a monospaced font
|
|
||||||
// in a terminal of a particular width, or optionally with no width limit.
|
|
||||||
//
|
|
||||||
// The given width may be zero to disable word-wrapping of the detail text
|
|
||||||
// and truncation of source code snippets.
|
|
||||||
//
|
|
||||||
// If color is set to true, the output will include VT100 escape sequences to
|
|
||||||
// color-code the severity indicators. It is suggested to turn this off if
|
|
||||||
// the target writer is not a terminal.
|
|
||||||
func NewDiagnosticTextWriter(wr io.Writer, files map[string]*File, width uint, color bool) DiagnosticWriter {
|
|
||||||
return &diagnosticTextWriter{
|
|
||||||
files: files,
|
|
||||||
wr: wr,
|
|
||||||
width: width,
|
|
||||||
color: color,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *diagnosticTextWriter) WriteDiagnostic(diag *Diagnostic) error {
|
|
||||||
if diag == nil {
|
|
||||||
return errors.New("nil diagnostic")
|
|
||||||
}
|
|
||||||
|
|
||||||
var colorCode, highlightCode, resetCode string
|
|
||||||
if w.color {
|
|
||||||
switch diag.Severity {
|
|
||||||
case DiagError:
|
|
||||||
colorCode = "\x1b[31m"
|
|
||||||
case DiagWarning:
|
|
||||||
colorCode = "\x1b[33m"
|
|
||||||
}
|
|
||||||
resetCode = "\x1b[0m"
|
|
||||||
highlightCode = "\x1b[1;4m"
|
|
||||||
}
|
|
||||||
|
|
||||||
var severityStr string
|
|
||||||
switch diag.Severity {
|
|
||||||
case DiagError:
|
|
||||||
severityStr = "Error"
|
|
||||||
case DiagWarning:
|
|
||||||
severityStr = "Warning"
|
|
||||||
default:
|
|
||||||
// should never happen
|
|
||||||
severityStr = "???????"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(w.wr, "%s%s%s: %s\n\n", colorCode, severityStr, resetCode, diag.Summary)
|
|
||||||
|
|
||||||
if diag.Subject != nil {
|
|
||||||
snipRange := *diag.Subject
|
|
||||||
highlightRange := snipRange
|
|
||||||
if diag.Context != nil {
|
|
||||||
// Show enough of the source code to include both the subject
|
|
||||||
// and context ranges, which overlap in all reasonable
|
|
||||||
// situations.
|
|
||||||
snipRange = RangeOver(snipRange, *diag.Context)
|
|
||||||
}
|
|
||||||
// We can't illustrate an empty range, so we'll turn such ranges into
|
|
||||||
// single-character ranges, which might not be totally valid (may point
|
|
||||||
// off the end of a line, or off the end of the file) but are good
|
|
||||||
// enough for the bounds checks we do below.
|
|
||||||
if snipRange.Empty() {
|
|
||||||
snipRange.End.Byte++
|
|
||||||
snipRange.End.Column++
|
|
||||||
}
|
|
||||||
if highlightRange.Empty() {
|
|
||||||
highlightRange.End.Byte++
|
|
||||||
highlightRange.End.Column++
|
|
||||||
}
|
|
||||||
|
|
||||||
file := w.files[diag.Subject.Filename]
|
|
||||||
if file == nil || file.Bytes == nil {
|
|
||||||
fmt.Fprintf(w.wr, " on %s line %d:\n (source code not available)\n\n", diag.Subject.Filename, diag.Subject.Start.Line)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
var contextLine string
|
|
||||||
if diag.Subject != nil {
|
|
||||||
contextLine = contextString(file, diag.Subject.Start.Byte)
|
|
||||||
if contextLine != "" {
|
|
||||||
contextLine = ", in " + contextLine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(w.wr, " on %s line %d%s:\n", diag.Subject.Filename, diag.Subject.Start.Line, contextLine)
|
|
||||||
|
|
||||||
src := file.Bytes
|
|
||||||
sc := NewRangeScanner(src, diag.Subject.Filename, bufio.ScanLines)
|
|
||||||
|
|
||||||
for sc.Scan() {
|
|
||||||
lineRange := sc.Range()
|
|
||||||
if !lineRange.Overlaps(snipRange) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeRange, highlightedRange, afterRange := lineRange.PartitionAround(highlightRange)
|
|
||||||
if highlightedRange.Empty() {
|
|
||||||
fmt.Fprintf(w.wr, "%4d: %s\n", lineRange.Start.Line, sc.Bytes())
|
|
||||||
} else {
|
|
||||||
before := beforeRange.SliceBytes(src)
|
|
||||||
highlighted := highlightedRange.SliceBytes(src)
|
|
||||||
after := afterRange.SliceBytes(src)
|
|
||||||
fmt.Fprintf(
|
|
||||||
w.wr, "%4d: %s%s%s%s%s\n",
|
|
||||||
lineRange.Start.Line,
|
|
||||||
before,
|
|
||||||
highlightCode, highlighted, resetCode,
|
|
||||||
after,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
w.wr.Write([]byte{'\n'})
|
|
||||||
}
|
|
||||||
|
|
||||||
if diag.Expression != nil && diag.EvalContext != nil {
|
|
||||||
// We will attempt to render the values for any variables
|
|
||||||
// referenced in the given expression as additional context, for
|
|
||||||
// situations where the same expression is evaluated multiple
|
|
||||||
// times in different scopes.
|
|
||||||
expr := diag.Expression
|
|
||||||
ctx := diag.EvalContext
|
|
||||||
|
|
||||||
vars := expr.Variables()
|
|
||||||
stmts := make([]string, 0, len(vars))
|
|
||||||
seen := make(map[string]struct{}, len(vars))
|
|
||||||
for _, traversal := range vars {
|
|
||||||
val, diags := traversal.TraverseAbs(ctx)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
// Skip anything that generates errors, since we probably
|
|
||||||
// already have the same error in our diagnostics set
|
|
||||||
// already.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
traversalStr := w.traversalStr(traversal)
|
|
||||||
if _, exists := seen[traversalStr]; exists {
|
|
||||||
continue // don't show duplicates when the same variable is referenced multiple times
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case !val.IsKnown():
|
|
||||||
// Can't say anything about this yet, then.
|
|
||||||
continue
|
|
||||||
case val.IsNull():
|
|
||||||
stmts = append(stmts, fmt.Sprintf("%s set to null", traversalStr))
|
|
||||||
default:
|
|
||||||
stmts = append(stmts, fmt.Sprintf("%s as %s", traversalStr, w.valueStr(val)))
|
|
||||||
}
|
|
||||||
seen[traversalStr] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(stmts) // FIXME: Should maybe use a traversal-aware sort that can sort numeric indexes properly?
|
|
||||||
last := len(stmts) - 1
|
|
||||||
|
|
||||||
for i, stmt := range stmts {
|
|
||||||
switch i {
|
|
||||||
case 0:
|
|
||||||
w.wr.Write([]byte{'w', 'i', 't', 'h', ' '})
|
|
||||||
default:
|
|
||||||
w.wr.Write([]byte{' ', ' ', ' ', ' ', ' '})
|
|
||||||
}
|
|
||||||
w.wr.Write([]byte(stmt))
|
|
||||||
switch i {
|
|
||||||
case last:
|
|
||||||
w.wr.Write([]byte{'.', '\n', '\n'})
|
|
||||||
default:
|
|
||||||
w.wr.Write([]byte{',', '\n'})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if diag.Detail != "" {
|
|
||||||
detail := diag.Detail
|
|
||||||
if w.width != 0 {
|
|
||||||
detail = wordwrap.WrapString(detail, w.width)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w.wr, "%s\n\n", detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *diagnosticTextWriter) WriteDiagnostics(diags Diagnostics) error {
|
|
||||||
for _, diag := range diags {
|
|
||||||
err := w.WriteDiagnostic(diag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *diagnosticTextWriter) traversalStr(traversal Traversal) string {
|
|
||||||
// This is a specialized subset of traversal rendering tailored to
|
|
||||||
// producing helpful contextual messages in diagnostics. It is not
|
|
||||||
// comprehensive nor intended to be used for other purposes.
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
for _, step := range traversal {
|
|
||||||
switch tStep := step.(type) {
|
|
||||||
case TraverseRoot:
|
|
||||||
buf.WriteString(tStep.Name)
|
|
||||||
case TraverseAttr:
|
|
||||||
buf.WriteByte('.')
|
|
||||||
buf.WriteString(tStep.Name)
|
|
||||||
case TraverseIndex:
|
|
||||||
buf.WriteByte('[')
|
|
||||||
if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() {
|
|
||||||
buf.WriteString(w.valueStr(tStep.Key))
|
|
||||||
} else {
|
|
||||||
// We'll just use a placeholder for more complex values,
|
|
||||||
// since otherwise our result could grow ridiculously long.
|
|
||||||
buf.WriteString("...")
|
|
||||||
}
|
|
||||||
buf.WriteByte(']')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *diagnosticTextWriter) valueStr(val cty.Value) string {
|
|
||||||
// This is a specialized subset of value rendering tailored to producing
|
|
||||||
// helpful but concise messages in diagnostics. It is not comprehensive
|
|
||||||
// nor intended to be used for other purposes.
|
|
||||||
|
|
||||||
ty := val.Type()
|
|
||||||
switch {
|
|
||||||
case val.IsNull():
|
|
||||||
return "null"
|
|
||||||
case !val.IsKnown():
|
|
||||||
// Should never happen here because we should filter before we get
|
|
||||||
// in here, but we'll do something reasonable rather than panic.
|
|
||||||
return "(not yet known)"
|
|
||||||
case ty == cty.Bool:
|
|
||||||
if val.True() {
|
|
||||||
return "true"
|
|
||||||
}
|
|
||||||
return "false"
|
|
||||||
case ty == cty.Number:
|
|
||||||
bf := val.AsBigFloat()
|
|
||||||
return bf.Text('g', 10)
|
|
||||||
case ty == cty.String:
|
|
||||||
// Go string syntax is not exactly the same as HCL native string syntax,
|
|
||||||
// but we'll accept the minor edge-cases where this is different here
|
|
||||||
// for now, just to get something reasonable here.
|
|
||||||
return fmt.Sprintf("%q", val.AsString())
|
|
||||||
case ty.IsCollectionType() || ty.IsTupleType():
|
|
||||||
l := val.LengthInt()
|
|
||||||
switch l {
|
|
||||||
case 0:
|
|
||||||
return "empty " + ty.FriendlyName()
|
|
||||||
case 1:
|
|
||||||
return ty.FriendlyName() + " with 1 element"
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l)
|
|
||||||
}
|
|
||||||
case ty.IsObjectType():
|
|
||||||
atys := ty.AttributeTypes()
|
|
||||||
l := len(atys)
|
|
||||||
switch l {
|
|
||||||
case 0:
|
|
||||||
return "object with no attributes"
|
|
||||||
case 1:
|
|
||||||
var name string
|
|
||||||
for k := range atys {
|
|
||||||
name = k
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("object with 1 attribute %q", name)
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("object with %d attributes", l)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return ty.FriendlyName()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func contextString(file *File, offset int) string {
|
|
||||||
type contextStringer interface {
|
|
||||||
ContextString(offset int) string
|
|
||||||
}
|
|
||||||
|
|
||||||
if cser, ok := file.Nav.(contextStringer); ok {
|
|
||||||
return cser.ContextString(offset)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/agext/levenshtein"
|
|
||||||
)
|
|
||||||
|
|
||||||
// nameSuggestion tries to find a name from the given slice of suggested names
|
|
||||||
// that is close to the given name and returns it if found. If no suggestion
|
|
||||||
// is close enough, returns the empty string.
|
|
||||||
//
|
|
||||||
// The suggestions are tried in order, so earlier suggestions take precedence
|
|
||||||
// if the given string is similar to two or more suggestions.
|
|
||||||
//
|
|
||||||
// This function is intended to be used with a relatively-small number of
|
|
||||||
// suggestions. It's not optimized for hundreds or thousands of them.
|
|
||||||
func nameSuggestion(given string, suggestions []string) string {
|
|
||||||
for _, suggestion := range suggestions {
|
|
||||||
dist := levenshtein.Distance(given, suggestion, nil)
|
|
||||||
if dist < 3 { // threshold determined experimentally
|
|
||||||
return suggestion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
package hcl
|
|
|
@ -1,25 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
"github.com/zclconf/go-cty/cty/function"
|
|
||||||
)
|
|
||||||
|
|
||||||
// An EvalContext provides the variables and functions that should be used
|
|
||||||
// to evaluate an expression.
|
|
||||||
type EvalContext struct {
|
|
||||||
Variables map[string]cty.Value
|
|
||||||
Functions map[string]function.Function
|
|
||||||
parent *EvalContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewChild returns a new EvalContext that is a child of the receiver.
|
|
||||||
func (ctx *EvalContext) NewChild() *EvalContext {
|
|
||||||
return &EvalContext{parent: ctx}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parent returns the parent of the receiver, or nil if the receiver has
|
|
||||||
// no parent.
|
|
||||||
func (ctx *EvalContext) Parent() *EvalContext {
|
|
||||||
return ctx.parent
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
// ExprCall tests if the given expression is a function call and,
|
|
||||||
// if so, extracts the function name and the expressions that represent
|
|
||||||
// the arguments. If the given expression is not statically a function call,
|
|
||||||
// error diagnostics are returned.
|
|
||||||
//
|
|
||||||
// A particular Expression implementation can support this function by
|
|
||||||
// offering a method called ExprCall that takes no arguments and returns
|
|
||||||
// *StaticCall. This method should return nil if a static call cannot
|
|
||||||
// be extracted. Alternatively, an implementation can support
|
|
||||||
// UnwrapExpression to delegate handling of this function to a wrapped
|
|
||||||
// Expression object.
|
|
||||||
func ExprCall(expr Expression) (*StaticCall, Diagnostics) {
|
|
||||||
type exprCall interface {
|
|
||||||
ExprCall() *StaticCall
|
|
||||||
}
|
|
||||||
|
|
||||||
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
|
|
||||||
_, supported := expr.(exprCall)
|
|
||||||
return supported
|
|
||||||
})
|
|
||||||
|
|
||||||
if exC, supported := physExpr.(exprCall); supported {
|
|
||||||
if call := exC.ExprCall(); call != nil {
|
|
||||||
return call, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, Diagnostics{
|
|
||||||
&Diagnostic{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Invalid expression",
|
|
||||||
Detail: "A static function call is required.",
|
|
||||||
Subject: expr.StartRange().Ptr(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StaticCall represents a function call that was extracted statically from
|
|
||||||
// an expression using ExprCall.
|
|
||||||
type StaticCall struct {
|
|
||||||
Name string
|
|
||||||
NameRange Range
|
|
||||||
Arguments []Expression
|
|
||||||
ArgsRange Range
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
// ExprList tests if the given expression is a static list construct and,
|
|
||||||
// if so, extracts the expressions that represent the list elements.
|
|
||||||
// If the given expression is not a static list, error diagnostics are
|
|
||||||
// returned.
|
|
||||||
//
|
|
||||||
// A particular Expression implementation can support this function by
|
|
||||||
// offering a method called ExprList that takes no arguments and returns
|
|
||||||
// []Expression. This method should return nil if a static list cannot
|
|
||||||
// be extracted. Alternatively, an implementation can support
|
|
||||||
// UnwrapExpression to delegate handling of this function to a wrapped
|
|
||||||
// Expression object.
|
|
||||||
func ExprList(expr Expression) ([]Expression, Diagnostics) {
|
|
||||||
type exprList interface {
|
|
||||||
ExprList() []Expression
|
|
||||||
}
|
|
||||||
|
|
||||||
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
|
|
||||||
_, supported := expr.(exprList)
|
|
||||||
return supported
|
|
||||||
})
|
|
||||||
|
|
||||||
if exL, supported := physExpr.(exprList); supported {
|
|
||||||
if list := exL.ExprList(); list != nil {
|
|
||||||
return list, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, Diagnostics{
|
|
||||||
&Diagnostic{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Invalid expression",
|
|
||||||
Detail: "A static list expression is required.",
|
|
||||||
Subject: expr.StartRange().Ptr(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
// ExprMap tests if the given expression is a static map construct and,
|
|
||||||
// if so, extracts the expressions that represent the map elements.
|
|
||||||
// If the given expression is not a static map, error diagnostics are
|
|
||||||
// returned.
|
|
||||||
//
|
|
||||||
// A particular Expression implementation can support this function by
|
|
||||||
// offering a method called ExprMap that takes no arguments and returns
|
|
||||||
// []KeyValuePair. This method should return nil if a static map cannot
|
|
||||||
// be extracted. Alternatively, an implementation can support
|
|
||||||
// UnwrapExpression to delegate handling of this function to a wrapped
|
|
||||||
// Expression object.
|
|
||||||
func ExprMap(expr Expression) ([]KeyValuePair, Diagnostics) {
|
|
||||||
type exprMap interface {
|
|
||||||
ExprMap() []KeyValuePair
|
|
||||||
}
|
|
||||||
|
|
||||||
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
|
|
||||||
_, supported := expr.(exprMap)
|
|
||||||
return supported
|
|
||||||
})
|
|
||||||
|
|
||||||
if exM, supported := physExpr.(exprMap); supported {
|
|
||||||
if pairs := exM.ExprMap(); pairs != nil {
|
|
||||||
return pairs, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, Diagnostics{
|
|
||||||
&Diagnostic{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Invalid expression",
|
|
||||||
Detail: "A static map expression is required.",
|
|
||||||
Subject: expr.StartRange().Ptr(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// KeyValuePair represents a pair of expressions that serve as a single item
|
|
||||||
// within a map or object definition construct.
|
|
||||||
type KeyValuePair struct {
|
|
||||||
Key Expression
|
|
||||||
Value Expression
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
type unwrapExpression interface {
|
|
||||||
UnwrapExpression() Expression
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnwrapExpression removes any "wrapper" expressions from the given expression,
|
|
||||||
// to recover the representation of the physical expression given in source
|
|
||||||
// code.
|
|
||||||
//
|
|
||||||
// Sometimes wrapping expressions are used to modify expression behavior, e.g.
|
|
||||||
// in extensions that need to make some local variables available to certain
|
|
||||||
// sub-trees of the configuration. This can make it difficult to reliably
|
|
||||||
// type-assert on the physical AST types used by the underlying syntax.
|
|
||||||
//
|
|
||||||
// Unwrapping an expression may modify its behavior by stripping away any
|
|
||||||
// additional constraints or capabilities being applied to the Value and
|
|
||||||
// Variables methods, so this function should generally only be used prior
|
|
||||||
// to operations that concern themselves with the static syntax of the input
|
|
||||||
// configuration, and not with the effective value of the expression.
|
|
||||||
//
|
|
||||||
// Wrapper expression types must support unwrapping by implementing a method
|
|
||||||
// called UnwrapExpression that takes no arguments and returns the embedded
|
|
||||||
// Expression. Implementations of this method should peel away only one level
|
|
||||||
// of wrapping, if multiple are present. This method may return nil to
|
|
||||||
// indicate _dynamically_ that no wrapped expression is available, for
|
|
||||||
// expression types that might only behave as wrappers in certain cases.
|
|
||||||
func UnwrapExpression(expr Expression) Expression {
|
|
||||||
for {
|
|
||||||
unwrap, wrapped := expr.(unwrapExpression)
|
|
||||||
if !wrapped {
|
|
||||||
return expr
|
|
||||||
}
|
|
||||||
innerExpr := unwrap.UnwrapExpression()
|
|
||||||
if innerExpr == nil {
|
|
||||||
return expr
|
|
||||||
}
|
|
||||||
expr = innerExpr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnwrapExpressionUntil is similar to UnwrapExpression except it gives the
|
|
||||||
// caller an opportunity to test each level of unwrapping to see each a
|
|
||||||
// particular expression is accepted.
|
|
||||||
//
|
|
||||||
// This could be used, for example, to unwrap until a particular other
|
|
||||||
// interface is satisfied, regardless of wrap wrapping level it is satisfied
|
|
||||||
// at.
|
|
||||||
//
|
|
||||||
// The given callback function must return false to continue wrapping, or
|
|
||||||
// true to accept and return the proposed expression given. If the callback
|
|
||||||
// function rejects even the final, physical expression then the result of
|
|
||||||
// this function is nil.
|
|
||||||
func UnwrapExpressionUntil(expr Expression, until func(Expression) bool) Expression {
|
|
||||||
for {
|
|
||||||
if until(expr) {
|
|
||||||
return expr
|
|
||||||
}
|
|
||||||
unwrap, wrapped := expr.(unwrapExpression)
|
|
||||||
if !wrapped {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
expr = unwrap.UnwrapExpression()
|
|
||||||
if expr == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// setDiagEvalContext is an internal helper that will impose a particular
|
|
||||||
// EvalContext on a set of diagnostics in-place, for any diagnostic that
|
|
||||||
// does not already have an EvalContext set.
|
|
||||||
//
|
|
||||||
// We generally expect diagnostics to be immutable, but this is safe to use
|
|
||||||
// on any Diagnostics where none of the contained Diagnostic objects have yet
|
|
||||||
// been seen by a caller. Its purpose is to apply additional context to a
|
|
||||||
// set of diagnostics produced by a "deeper" component as the stack unwinds
|
|
||||||
// during expression evaluation.
|
|
||||||
func setDiagEvalContext(diags hcl.Diagnostics, expr hcl.Expression, ctx *hcl.EvalContext) {
|
|
||||||
for _, diag := range diags {
|
|
||||||
if diag.Expression == nil {
|
|
||||||
diag.Expression = expr
|
|
||||||
diag.EvalContext = ctx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/agext/levenshtein"
|
|
||||||
)
|
|
||||||
|
|
||||||
// nameSuggestion tries to find a name from the given slice of suggested names
|
|
||||||
// that is close to the given name and returns it if found. If no suggestion
|
|
||||||
// is close enough, returns the empty string.
|
|
||||||
//
|
|
||||||
// The suggestions are tried in order, so earlier suggestions take precedence
|
|
||||||
// if the given string is similar to two or more suggestions.
|
|
||||||
//
|
|
||||||
// This function is intended to be used with a relatively-small number of
|
|
||||||
// suggestions. It's not optimized for hundreds or thousands of them.
|
|
||||||
func nameSuggestion(given string, suggestions []string) string {
|
|
||||||
for _, suggestion := range suggestions {
|
|
||||||
dist := levenshtein.Distance(given, suggestion, nil)
|
|
||||||
if dist < 3 { // threshold determined experimentally
|
|
||||||
return suggestion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
// Package hclsyntax contains the parser, AST, etc for HCL's native language,
|
|
||||||
// as opposed to the JSON variant.
|
|
||||||
//
|
|
||||||
// In normal use applications should rarely depend on this package directly,
|
|
||||||
// instead preferring the higher-level interface of the main hcl package and
|
|
||||||
// its companion package hclparse.
|
|
||||||
package hclsyntax
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,268 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
"github.com/zclconf/go-cty/cty/convert"
|
|
||||||
"github.com/zclconf/go-cty/cty/function"
|
|
||||||
"github.com/zclconf/go-cty/cty/function/stdlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Operation struct {
|
|
||||||
Impl function.Function
|
|
||||||
Type cty.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
OpLogicalOr = &Operation{
|
|
||||||
Impl: stdlib.OrFunc,
|
|
||||||
Type: cty.Bool,
|
|
||||||
}
|
|
||||||
OpLogicalAnd = &Operation{
|
|
||||||
Impl: stdlib.AndFunc,
|
|
||||||
Type: cty.Bool,
|
|
||||||
}
|
|
||||||
OpLogicalNot = &Operation{
|
|
||||||
Impl: stdlib.NotFunc,
|
|
||||||
Type: cty.Bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
OpEqual = &Operation{
|
|
||||||
Impl: stdlib.EqualFunc,
|
|
||||||
Type: cty.Bool,
|
|
||||||
}
|
|
||||||
OpNotEqual = &Operation{
|
|
||||||
Impl: stdlib.NotEqualFunc,
|
|
||||||
Type: cty.Bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
OpGreaterThan = &Operation{
|
|
||||||
Impl: stdlib.GreaterThanFunc,
|
|
||||||
Type: cty.Bool,
|
|
||||||
}
|
|
||||||
OpGreaterThanOrEqual = &Operation{
|
|
||||||
Impl: stdlib.GreaterThanOrEqualToFunc,
|
|
||||||
Type: cty.Bool,
|
|
||||||
}
|
|
||||||
OpLessThan = &Operation{
|
|
||||||
Impl: stdlib.LessThanFunc,
|
|
||||||
Type: cty.Bool,
|
|
||||||
}
|
|
||||||
OpLessThanOrEqual = &Operation{
|
|
||||||
Impl: stdlib.LessThanOrEqualToFunc,
|
|
||||||
Type: cty.Bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
OpAdd = &Operation{
|
|
||||||
Impl: stdlib.AddFunc,
|
|
||||||
Type: cty.Number,
|
|
||||||
}
|
|
||||||
OpSubtract = &Operation{
|
|
||||||
Impl: stdlib.SubtractFunc,
|
|
||||||
Type: cty.Number,
|
|
||||||
}
|
|
||||||
OpMultiply = &Operation{
|
|
||||||
Impl: stdlib.MultiplyFunc,
|
|
||||||
Type: cty.Number,
|
|
||||||
}
|
|
||||||
OpDivide = &Operation{
|
|
||||||
Impl: stdlib.DivideFunc,
|
|
||||||
Type: cty.Number,
|
|
||||||
}
|
|
||||||
OpModulo = &Operation{
|
|
||||||
Impl: stdlib.ModuloFunc,
|
|
||||||
Type: cty.Number,
|
|
||||||
}
|
|
||||||
OpNegate = &Operation{
|
|
||||||
Impl: stdlib.NegateFunc,
|
|
||||||
Type: cty.Number,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var binaryOps []map[TokenType]*Operation
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// This operation table maps from the operator's token type
|
|
||||||
// to the AST operation type. All expressions produced from
|
|
||||||
// binary operators are BinaryOp nodes.
|
|
||||||
//
|
|
||||||
// Binary operator groups are listed in order of precedence, with
|
|
||||||
// the *lowest* precedence first. Operators within the same group
|
|
||||||
// have left-to-right associativity.
|
|
||||||
binaryOps = []map[TokenType]*Operation{
|
|
||||||
{
|
|
||||||
TokenOr: OpLogicalOr,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TokenAnd: OpLogicalAnd,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TokenEqualOp: OpEqual,
|
|
||||||
TokenNotEqual: OpNotEqual,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TokenGreaterThan: OpGreaterThan,
|
|
||||||
TokenGreaterThanEq: OpGreaterThanOrEqual,
|
|
||||||
TokenLessThan: OpLessThan,
|
|
||||||
TokenLessThanEq: OpLessThanOrEqual,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TokenPlus: OpAdd,
|
|
||||||
TokenMinus: OpSubtract,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TokenStar: OpMultiply,
|
|
||||||
TokenSlash: OpDivide,
|
|
||||||
TokenPercent: OpModulo,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type BinaryOpExpr struct {
|
|
||||||
LHS Expression
|
|
||||||
Op *Operation
|
|
||||||
RHS Expression
|
|
||||||
|
|
||||||
SrcRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *BinaryOpExpr) walkChildNodes(w internalWalkFunc) {
|
|
||||||
w(e.LHS)
|
|
||||||
w(e.RHS)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *BinaryOpExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
||||||
impl := e.Op.Impl // assumed to be a function taking exactly two arguments
|
|
||||||
params := impl.Params()
|
|
||||||
lhsParam := params[0]
|
|
||||||
rhsParam := params[1]
|
|
||||||
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
|
|
||||||
givenLHSVal, lhsDiags := e.LHS.Value(ctx)
|
|
||||||
givenRHSVal, rhsDiags := e.RHS.Value(ctx)
|
|
||||||
diags = append(diags, lhsDiags...)
|
|
||||||
diags = append(diags, rhsDiags...)
|
|
||||||
|
|
||||||
lhsVal, err := convert.Convert(givenLHSVal, lhsParam.Type)
|
|
||||||
if err != nil {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid operand",
|
|
||||||
Detail: fmt.Sprintf("Unsuitable value for left operand: %s.", err),
|
|
||||||
Subject: e.LHS.Range().Ptr(),
|
|
||||||
Context: &e.SrcRange,
|
|
||||||
Expression: e.LHS,
|
|
||||||
EvalContext: ctx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
rhsVal, err := convert.Convert(givenRHSVal, rhsParam.Type)
|
|
||||||
if err != nil {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid operand",
|
|
||||||
Detail: fmt.Sprintf("Unsuitable value for right operand: %s.", err),
|
|
||||||
Subject: e.RHS.Range().Ptr(),
|
|
||||||
Context: &e.SrcRange,
|
|
||||||
Expression: e.RHS,
|
|
||||||
EvalContext: ctx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if diags.HasErrors() {
|
|
||||||
// Don't actually try the call if we have errors already, since the
|
|
||||||
// this will probably just produce a confusing duplicative diagnostic.
|
|
||||||
return cty.UnknownVal(e.Op.Type), diags
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []cty.Value{lhsVal, rhsVal}
|
|
||||||
result, err := impl.Call(args)
|
|
||||||
if err != nil {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
// FIXME: This diagnostic is useless.
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Operation failed",
|
|
||||||
Detail: fmt.Sprintf("Error during operation: %s.", err),
|
|
||||||
Subject: &e.SrcRange,
|
|
||||||
Expression: e,
|
|
||||||
EvalContext: ctx,
|
|
||||||
})
|
|
||||||
return cty.UnknownVal(e.Op.Type), diags
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *BinaryOpExpr) Range() hcl.Range {
|
|
||||||
return e.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *BinaryOpExpr) StartRange() hcl.Range {
|
|
||||||
return e.LHS.StartRange()
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnaryOpExpr struct {
|
|
||||||
Op *Operation
|
|
||||||
Val Expression
|
|
||||||
|
|
||||||
SrcRange hcl.Range
|
|
||||||
SymbolRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UnaryOpExpr) walkChildNodes(w internalWalkFunc) {
|
|
||||||
w(e.Val)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UnaryOpExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
||||||
impl := e.Op.Impl // assumed to be a function taking exactly one argument
|
|
||||||
params := impl.Params()
|
|
||||||
param := params[0]
|
|
||||||
|
|
||||||
givenVal, diags := e.Val.Value(ctx)
|
|
||||||
|
|
||||||
val, err := convert.Convert(givenVal, param.Type)
|
|
||||||
if err != nil {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid operand",
|
|
||||||
Detail: fmt.Sprintf("Unsuitable value for unary operand: %s.", err),
|
|
||||||
Subject: e.Val.Range().Ptr(),
|
|
||||||
Context: &e.SrcRange,
|
|
||||||
Expression: e.Val,
|
|
||||||
EvalContext: ctx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if diags.HasErrors() {
|
|
||||||
// Don't actually try the call if we have errors already, since the
|
|
||||||
// this will probably just produce a confusing duplicative diagnostic.
|
|
||||||
return cty.UnknownVal(e.Op.Type), diags
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []cty.Value{val}
|
|
||||||
result, err := impl.Call(args)
|
|
||||||
if err != nil {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
// FIXME: This diagnostic is useless.
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Operation failed",
|
|
||||||
Detail: fmt.Sprintf("Error during operation: %s.", err),
|
|
||||||
Subject: &e.SrcRange,
|
|
||||||
Expression: e,
|
|
||||||
EvalContext: ctx,
|
|
||||||
})
|
|
||||||
return cty.UnknownVal(e.Op.Type), diags
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UnaryOpExpr) Range() hcl.Range {
|
|
||||||
return e.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UnaryOpExpr) StartRange() hcl.Range {
|
|
||||||
return e.SymbolRange
|
|
||||||
}
|
|
|
@ -1,220 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
"github.com/zclconf/go-cty/cty/convert"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TemplateExpr struct {
|
|
||||||
Parts []Expression
|
|
||||||
|
|
||||||
SrcRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateExpr) walkChildNodes(w internalWalkFunc) {
|
|
||||||
for _, part := range e.Parts {
|
|
||||||
w(part)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
isKnown := true
|
|
||||||
|
|
||||||
for _, part := range e.Parts {
|
|
||||||
partVal, partDiags := part.Value(ctx)
|
|
||||||
diags = append(diags, partDiags...)
|
|
||||||
|
|
||||||
if partVal.IsNull() {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid template interpolation value",
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"The expression result is null. Cannot include a null value in a string template.",
|
|
||||||
),
|
|
||||||
Subject: part.Range().Ptr(),
|
|
||||||
Context: &e.SrcRange,
|
|
||||||
Expression: part,
|
|
||||||
EvalContext: ctx,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !partVal.IsKnown() {
|
|
||||||
// If any part is unknown then the result as a whole must be
|
|
||||||
// unknown too. We'll keep on processing the rest of the parts
|
|
||||||
// anyway, because we want to still emit any diagnostics resulting
|
|
||||||
// from evaluating those.
|
|
||||||
isKnown = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
strVal, err := convert.Convert(partVal, cty.String)
|
|
||||||
if err != nil {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid template interpolation value",
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"Cannot include the given value in a string template: %s.",
|
|
||||||
err.Error(),
|
|
||||||
),
|
|
||||||
Subject: part.Range().Ptr(),
|
|
||||||
Context: &e.SrcRange,
|
|
||||||
Expression: part,
|
|
||||||
EvalContext: ctx,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString(strVal.AsString())
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isKnown {
|
|
||||||
return cty.UnknownVal(cty.String), diags
|
|
||||||
}
|
|
||||||
|
|
||||||
return cty.StringVal(buf.String()), diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateExpr) Range() hcl.Range {
|
|
||||||
return e.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateExpr) StartRange() hcl.Range {
|
|
||||||
return e.Parts[0].StartRange()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsStringLiteral returns true if and only if the template consists only of
|
|
||||||
// single string literal, as would be created for a simple quoted string like
|
|
||||||
// "foo".
|
|
||||||
//
|
|
||||||
// If this function returns true, then calling Value on the same expression
|
|
||||||
// with a nil EvalContext will return the literal value.
|
|
||||||
//
|
|
||||||
// Note that "${"foo"}", "${1}", etc aren't considered literal values for the
|
|
||||||
// purposes of this method, because the intent of this method is to identify
|
|
||||||
// situations where the user seems to be explicitly intending literal string
|
|
||||||
// interpretation, not situations that result in literals as a technicality
|
|
||||||
// of the template expression unwrapping behavior.
|
|
||||||
func (e *TemplateExpr) IsStringLiteral() bool {
|
|
||||||
if len(e.Parts) != 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, ok := e.Parts[0].(*LiteralValueExpr)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateJoinExpr is used to convert tuples of strings produced by template
|
|
||||||
// constructs (i.e. for loops) into flat strings, by converting the values
|
|
||||||
// tos strings and joining them. This AST node is not used directly; it's
|
|
||||||
// produced as part of the AST of a "for" loop in a template.
|
|
||||||
type TemplateJoinExpr struct {
|
|
||||||
Tuple Expression
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateJoinExpr) walkChildNodes(w internalWalkFunc) {
|
|
||||||
w(e.Tuple)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateJoinExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
||||||
tuple, diags := e.Tuple.Value(ctx)
|
|
||||||
|
|
||||||
if tuple.IsNull() {
|
|
||||||
// This indicates a bug in the code that constructed the AST.
|
|
||||||
panic("TemplateJoinExpr got null tuple")
|
|
||||||
}
|
|
||||||
if tuple.Type() == cty.DynamicPseudoType {
|
|
||||||
return cty.UnknownVal(cty.String), diags
|
|
||||||
}
|
|
||||||
if !tuple.Type().IsTupleType() {
|
|
||||||
// This indicates a bug in the code that constructed the AST.
|
|
||||||
panic("TemplateJoinExpr got non-tuple tuple")
|
|
||||||
}
|
|
||||||
if !tuple.IsKnown() {
|
|
||||||
return cty.UnknownVal(cty.String), diags
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
it := tuple.ElementIterator()
|
|
||||||
for it.Next() {
|
|
||||||
_, val := it.Element()
|
|
||||||
|
|
||||||
if val.IsNull() {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid template interpolation value",
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"An iteration result is null. Cannot include a null value in a string template.",
|
|
||||||
),
|
|
||||||
Subject: e.Range().Ptr(),
|
|
||||||
Expression: e,
|
|
||||||
EvalContext: ctx,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if val.Type() == cty.DynamicPseudoType {
|
|
||||||
return cty.UnknownVal(cty.String), diags
|
|
||||||
}
|
|
||||||
strVal, err := convert.Convert(val, cty.String)
|
|
||||||
if err != nil {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid template interpolation value",
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"Cannot include one of the interpolation results into the string template: %s.",
|
|
||||||
err.Error(),
|
|
||||||
),
|
|
||||||
Subject: e.Range().Ptr(),
|
|
||||||
Expression: e,
|
|
||||||
EvalContext: ctx,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !val.IsKnown() {
|
|
||||||
return cty.UnknownVal(cty.String), diags
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString(strVal.AsString())
|
|
||||||
}
|
|
||||||
|
|
||||||
return cty.StringVal(buf.String()), diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateJoinExpr) Range() hcl.Range {
|
|
||||||
return e.Tuple.Range()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateJoinExpr) StartRange() hcl.Range {
|
|
||||||
return e.Tuple.StartRange()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateWrapExpr is used instead of a TemplateExpr when a template
|
|
||||||
// consists _only_ of a single interpolation sequence. In that case, the
|
|
||||||
// template's result is the single interpolation's result, verbatim with
|
|
||||||
// no type conversions.
|
|
||||||
type TemplateWrapExpr struct {
|
|
||||||
Wrapped Expression
|
|
||||||
|
|
||||||
SrcRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateWrapExpr) walkChildNodes(w internalWalkFunc) {
|
|
||||||
w(e.Wrapped)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateWrapExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
||||||
return e.Wrapped.Value(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateWrapExpr) Range() hcl.Range {
|
|
||||||
return e.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateWrapExpr) StartRange() hcl.Range {
|
|
||||||
return e.SrcRange
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
// Generated by expression_vars_get.go. DO NOT EDIT.
|
|
||||||
// Run 'go generate' on this package to update the set of functions here.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (e *AnonSymbolExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *BinaryOpExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ConditionalExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ForExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *FunctionCallExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *IndexExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *LiteralValueExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ObjectConsExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ObjectConsKeyExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *RelativeTraversalExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ScopeTraversalExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *SplatExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateJoinExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TemplateWrapExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *TupleConsExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *UnaryOpExpr) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
// This is a 'go generate'-oriented program for producing the "Variables"
|
|
||||||
// method on every Expression implementation found within this package.
|
|
||||||
// All expressions share the same implementation for this method, which
|
|
||||||
// just wraps the package-level function "Variables" and uses an AST walk
|
|
||||||
// to do its work.
|
|
||||||
|
|
||||||
// +build ignore
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"go/ast"
|
|
||||||
"go/parser"
|
|
||||||
"go/token"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fs := token.NewFileSet()
|
|
||||||
pkgs, err := parser.ParseDir(fs, ".", nil, 0)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "error while parsing: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
pkg := pkgs["hclsyntax"]
|
|
||||||
|
|
||||||
// Walk all the files and collect the receivers of any "Value" methods
|
|
||||||
// that look like they are trying to implement Expression.
|
|
||||||
var recvs []string
|
|
||||||
for _, f := range pkg.Files {
|
|
||||||
for _, decl := range f.Decls {
|
|
||||||
fd, ok := decl.(*ast.FuncDecl)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fd.Name.Name != "Value" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
results := fd.Type.Results.List
|
|
||||||
if len(results) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
valResult := fd.Type.Results.List[0].Type.(*ast.SelectorExpr).X.(*ast.Ident)
|
|
||||||
diagsResult := fd.Type.Results.List[1].Type.(*ast.SelectorExpr).X.(*ast.Ident)
|
|
||||||
|
|
||||||
if valResult.Name != "cty" && diagsResult.Name != "hcl" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a method called Value and it returns something in
|
|
||||||
// "cty" followed by something in "hcl" then that's specific enough
|
|
||||||
// for now, even though this is not 100% exact as a correct
|
|
||||||
// implementation of Value.
|
|
||||||
|
|
||||||
recvTy := fd.Recv.List[0].Type
|
|
||||||
|
|
||||||
switch rtt := recvTy.(type) {
|
|
||||||
case *ast.StarExpr:
|
|
||||||
name := rtt.X.(*ast.Ident).Name
|
|
||||||
recvs = append(recvs, fmt.Sprintf("*%s", name))
|
|
||||||
default:
|
|
||||||
fmt.Fprintf(os.Stderr, "don't know what to do with a %T receiver\n", recvTy)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(recvs)
|
|
||||||
|
|
||||||
of, err := os.OpenFile("expression_vars.go", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to open output file: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprint(of, outputPreamble)
|
|
||||||
for _, recv := range recvs {
|
|
||||||
fmt.Fprintf(of, outputMethodFmt, recv)
|
|
||||||
}
|
|
||||||
fmt.Fprint(of, "\n")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputPreamble = `package hclsyntax
|
|
||||||
|
|
||||||
// Generated by expression_vars_get.go. DO NOT EDIT.
|
|
||||||
// Run 'go generate' on this package to update the set of functions here.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)`
|
|
||||||
|
|
||||||
const outputMethodFmt = `
|
|
||||||
|
|
||||||
func (e %s) Variables() []hcl.Traversal {
|
|
||||||
return Variables(e)
|
|
||||||
}`
|
|
|
@ -1,20 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// File is the top-level object resulting from parsing a configuration file.
|
|
||||||
type File struct {
|
|
||||||
Body *Body
|
|
||||||
Bytes []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *File) AsHCLFile() *hcl.File {
|
|
||||||
return &hcl.File{
|
|
||||||
Body: f.Body,
|
|
||||||
Bytes: f.Bytes,
|
|
||||||
|
|
||||||
// TODO: The Nav object, once we have an implementation of it
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
//go:generate go run expression_vars_gen.go
|
|
||||||
//go:generate ruby unicode2ragel.rb --url=http://www.unicode.org/Public/9.0.0/ucd/DerivedCoreProperties.txt -m UnicodeDerived -p ID_Start,ID_Continue -o unicode_derived.rl
|
|
||||||
//go:generate ragel -Z scan_tokens.rl
|
|
||||||
//go:generate gofmt -w scan_tokens.go
|
|
||||||
//go:generate ragel -Z scan_string_lit.rl
|
|
||||||
//go:generate gofmt -w scan_string_lit.go
|
|
||||||
//go:generate stringer -type TokenType -output token_type_string.go
|
|
|
@ -1,21 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Keyword []byte
|
|
||||||
|
|
||||||
var forKeyword = Keyword([]byte{'f', 'o', 'r'})
|
|
||||||
var inKeyword = Keyword([]byte{'i', 'n'})
|
|
||||||
var ifKeyword = Keyword([]byte{'i', 'f'})
|
|
||||||
var elseKeyword = Keyword([]byte{'e', 'l', 's', 'e'})
|
|
||||||
var endifKeyword = Keyword([]byte{'e', 'n', 'd', 'i', 'f'})
|
|
||||||
var endforKeyword = Keyword([]byte{'e', 'n', 'd', 'f', 'o', 'r'})
|
|
||||||
|
|
||||||
func (kw Keyword) TokenMatches(token Token) bool {
|
|
||||||
if token.Type != TokenIdent {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return bytes.Equal([]byte(kw), token.Bytes)
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
type navigation struct {
|
|
||||||
root *Body
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation of hcled.ContextString
|
|
||||||
func (n navigation) ContextString(offset int) string {
|
|
||||||
// We will walk our top-level blocks until we find one that contains
|
|
||||||
// the given offset, and then construct a representation of the header
|
|
||||||
// of the block.
|
|
||||||
|
|
||||||
var block *Block
|
|
||||||
for _, candidate := range n.root.Blocks {
|
|
||||||
if candidate.Range().ContainsOffset(offset) {
|
|
||||||
block = candidate
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if block == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(block.Labels) == 0 {
|
|
||||||
// Easy case!
|
|
||||||
return block.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
buf.WriteString(block.Type)
|
|
||||||
for _, label := range block.Labels {
|
|
||||||
fmt.Fprintf(buf, " %q", label)
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n navigation) ContextDefRange(offset int) hcl.Range {
|
|
||||||
var block *Block
|
|
||||||
for _, candidate := range n.root.Blocks {
|
|
||||||
if candidate.Range().ContainsOffset(offset) {
|
|
||||||
block = candidate
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if block == nil {
|
|
||||||
return hcl.Range{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return block.DefRange()
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Node is the abstract type that every AST node implements.
|
|
||||||
//
|
|
||||||
// This is a closed interface, so it cannot be implemented from outside of
|
|
||||||
// this package.
|
|
||||||
type Node interface {
|
|
||||||
// This is the mechanism by which the public-facing walk functions
|
|
||||||
// are implemented. Implementations should call the given function
|
|
||||||
// for each child node and then replace that node with its return value.
|
|
||||||
// The return value might just be the same node, for non-transforming
|
|
||||||
// walks.
|
|
||||||
walkChildNodes(w internalWalkFunc)
|
|
||||||
|
|
||||||
Range() hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
type internalWalkFunc func(Node)
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,799 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/apparentlymart/go-textseg/textseg"
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *parser) ParseTemplate() (Expression, hcl.Diagnostics) {
|
|
||||||
return p.parseTemplate(TokenEOF, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) parseTemplate(end TokenType, flushHeredoc bool) (Expression, hcl.Diagnostics) {
|
|
||||||
exprs, passthru, rng, diags := p.parseTemplateInner(end, flushHeredoc)
|
|
||||||
|
|
||||||
if passthru {
|
|
||||||
if len(exprs) != 1 {
|
|
||||||
panic("passthru set with len(exprs) != 1")
|
|
||||||
}
|
|
||||||
return &TemplateWrapExpr{
|
|
||||||
Wrapped: exprs[0],
|
|
||||||
SrcRange: rng,
|
|
||||||
}, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
return &TemplateExpr{
|
|
||||||
Parts: exprs,
|
|
||||||
SrcRange: rng,
|
|
||||||
}, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) parseTemplateInner(end TokenType, flushHeredoc bool) ([]Expression, bool, hcl.Range, hcl.Diagnostics) {
|
|
||||||
parts, diags := p.parseTemplateParts(end)
|
|
||||||
if flushHeredoc {
|
|
||||||
flushHeredocTemplateParts(parts) // Trim off leading spaces on lines per the flush heredoc spec
|
|
||||||
}
|
|
||||||
tp := templateParser{
|
|
||||||
Tokens: parts.Tokens,
|
|
||||||
SrcRange: parts.SrcRange,
|
|
||||||
}
|
|
||||||
exprs, exprsDiags := tp.parseRoot()
|
|
||||||
diags = append(diags, exprsDiags...)
|
|
||||||
|
|
||||||
passthru := false
|
|
||||||
if len(parts.Tokens) == 2 { // one real token and one synthetic "end" token
|
|
||||||
if _, isInterp := parts.Tokens[0].(*templateInterpToken); isInterp {
|
|
||||||
passthru = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return exprs, passthru, parts.SrcRange, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateParser struct {
|
|
||||||
Tokens []templateToken
|
|
||||||
SrcRange hcl.Range
|
|
||||||
|
|
||||||
pos int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *templateParser) parseRoot() ([]Expression, hcl.Diagnostics) {
|
|
||||||
var exprs []Expression
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
|
|
||||||
for {
|
|
||||||
next := p.Peek()
|
|
||||||
if _, isEnd := next.(*templateEndToken); isEnd {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
expr, exprDiags := p.parseExpr()
|
|
||||||
diags = append(diags, exprDiags...)
|
|
||||||
exprs = append(exprs, expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return exprs, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *templateParser) parseExpr() (Expression, hcl.Diagnostics) {
|
|
||||||
next := p.Peek()
|
|
||||||
switch tok := next.(type) {
|
|
||||||
|
|
||||||
case *templateLiteralToken:
|
|
||||||
p.Read() // eat literal
|
|
||||||
return &LiteralValueExpr{
|
|
||||||
Val: cty.StringVal(tok.Val),
|
|
||||||
SrcRange: tok.SrcRange,
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
case *templateInterpToken:
|
|
||||||
p.Read() // eat interp
|
|
||||||
return tok.Expr, nil
|
|
||||||
|
|
||||||
case *templateIfToken:
|
|
||||||
return p.parseIf()
|
|
||||||
|
|
||||||
case *templateForToken:
|
|
||||||
return p.parseFor()
|
|
||||||
|
|
||||||
case *templateEndToken:
|
|
||||||
p.Read() // eat erroneous token
|
|
||||||
return errPlaceholderExpr(tok.SrcRange), hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
// This is a particularly unhelpful diagnostic, so callers
|
|
||||||
// should attempt to pre-empt it and produce a more helpful
|
|
||||||
// diagnostic that is context-aware.
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unexpected end of template",
|
|
||||||
Detail: "The control directives within this template are unbalanced.",
|
|
||||||
Subject: &tok.SrcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
case *templateEndCtrlToken:
|
|
||||||
p.Read() // eat erroneous token
|
|
||||||
return errPlaceholderExpr(tok.SrcRange), hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: fmt.Sprintf("Unexpected %s directive", tok.Name()),
|
|
||||||
Detail: "The control directives within this template are unbalanced.",
|
|
||||||
Subject: &tok.SrcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
// should never happen, because above should be exhaustive
|
|
||||||
panic(fmt.Sprintf("unhandled template token type %T", next))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *templateParser) parseIf() (Expression, hcl.Diagnostics) {
|
|
||||||
open := p.Read()
|
|
||||||
openIf, isIf := open.(*templateIfToken)
|
|
||||||
if !isIf {
|
|
||||||
// should never happen if caller is behaving
|
|
||||||
panic("parseIf called with peeker not pointing at if token")
|
|
||||||
}
|
|
||||||
|
|
||||||
var ifExprs, elseExprs []Expression
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
var endifRange hcl.Range
|
|
||||||
|
|
||||||
currentExprs := &ifExprs
|
|
||||||
Token:
|
|
||||||
for {
|
|
||||||
next := p.Peek()
|
|
||||||
if end, isEnd := next.(*templateEndToken); isEnd {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unexpected end of template",
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"The if directive at %s is missing its corresponding endif directive.",
|
|
||||||
openIf.SrcRange,
|
|
||||||
),
|
|
||||||
Subject: &end.SrcRange,
|
|
||||||
})
|
|
||||||
return errPlaceholderExpr(end.SrcRange), diags
|
|
||||||
}
|
|
||||||
if end, isCtrlEnd := next.(*templateEndCtrlToken); isCtrlEnd {
|
|
||||||
p.Read() // eat end directive
|
|
||||||
|
|
||||||
switch end.Type {
|
|
||||||
|
|
||||||
case templateElse:
|
|
||||||
if currentExprs == &ifExprs {
|
|
||||||
currentExprs = &elseExprs
|
|
||||||
continue Token
|
|
||||||
}
|
|
||||||
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unexpected else directive",
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"Already in the else clause for the if started at %s.",
|
|
||||||
openIf.SrcRange,
|
|
||||||
),
|
|
||||||
Subject: &end.SrcRange,
|
|
||||||
})
|
|
||||||
|
|
||||||
case templateEndIf:
|
|
||||||
endifRange = end.SrcRange
|
|
||||||
break Token
|
|
||||||
|
|
||||||
default:
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: fmt.Sprintf("Unexpected %s directive", end.Name()),
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"Expecting an endif directive for the if started at %s.",
|
|
||||||
openIf.SrcRange,
|
|
||||||
),
|
|
||||||
Subject: &end.SrcRange,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return errPlaceholderExpr(end.SrcRange), diags
|
|
||||||
}
|
|
||||||
|
|
||||||
expr, exprDiags := p.parseExpr()
|
|
||||||
diags = append(diags, exprDiags...)
|
|
||||||
*currentExprs = append(*currentExprs, expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ifExprs) == 0 {
|
|
||||||
ifExprs = append(ifExprs, &LiteralValueExpr{
|
|
||||||
Val: cty.StringVal(""),
|
|
||||||
SrcRange: hcl.Range{
|
|
||||||
Filename: openIf.SrcRange.Filename,
|
|
||||||
Start: openIf.SrcRange.End,
|
|
||||||
End: openIf.SrcRange.End,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(elseExprs) == 0 {
|
|
||||||
elseExprs = append(elseExprs, &LiteralValueExpr{
|
|
||||||
Val: cty.StringVal(""),
|
|
||||||
SrcRange: hcl.Range{
|
|
||||||
Filename: endifRange.Filename,
|
|
||||||
Start: endifRange.Start,
|
|
||||||
End: endifRange.Start,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
trueExpr := &TemplateExpr{
|
|
||||||
Parts: ifExprs,
|
|
||||||
SrcRange: hcl.RangeBetween(ifExprs[0].Range(), ifExprs[len(ifExprs)-1].Range()),
|
|
||||||
}
|
|
||||||
falseExpr := &TemplateExpr{
|
|
||||||
Parts: elseExprs,
|
|
||||||
SrcRange: hcl.RangeBetween(elseExprs[0].Range(), elseExprs[len(elseExprs)-1].Range()),
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ConditionalExpr{
|
|
||||||
Condition: openIf.CondExpr,
|
|
||||||
TrueResult: trueExpr,
|
|
||||||
FalseResult: falseExpr,
|
|
||||||
|
|
||||||
SrcRange: hcl.RangeBetween(openIf.SrcRange, endifRange),
|
|
||||||
}, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *templateParser) parseFor() (Expression, hcl.Diagnostics) {
|
|
||||||
open := p.Read()
|
|
||||||
openFor, isFor := open.(*templateForToken)
|
|
||||||
if !isFor {
|
|
||||||
// should never happen if caller is behaving
|
|
||||||
panic("parseFor called with peeker not pointing at for token")
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentExprs []Expression
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
var endforRange hcl.Range
|
|
||||||
|
|
||||||
Token:
|
|
||||||
for {
|
|
||||||
next := p.Peek()
|
|
||||||
if end, isEnd := next.(*templateEndToken); isEnd {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unexpected end of template",
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"The for directive at %s is missing its corresponding endfor directive.",
|
|
||||||
openFor.SrcRange,
|
|
||||||
),
|
|
||||||
Subject: &end.SrcRange,
|
|
||||||
})
|
|
||||||
return errPlaceholderExpr(end.SrcRange), diags
|
|
||||||
}
|
|
||||||
if end, isCtrlEnd := next.(*templateEndCtrlToken); isCtrlEnd {
|
|
||||||
p.Read() // eat end directive
|
|
||||||
|
|
||||||
switch end.Type {
|
|
||||||
|
|
||||||
case templateElse:
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unexpected else directive",
|
|
||||||
Detail: "An else clause is not expected for a for directive.",
|
|
||||||
Subject: &end.SrcRange,
|
|
||||||
})
|
|
||||||
|
|
||||||
case templateEndFor:
|
|
||||||
endforRange = end.SrcRange
|
|
||||||
break Token
|
|
||||||
|
|
||||||
default:
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: fmt.Sprintf("Unexpected %s directive", end.Name()),
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"Expecting an endfor directive corresponding to the for directive at %s.",
|
|
||||||
openFor.SrcRange,
|
|
||||||
),
|
|
||||||
Subject: &end.SrcRange,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return errPlaceholderExpr(end.SrcRange), diags
|
|
||||||
}
|
|
||||||
|
|
||||||
expr, exprDiags := p.parseExpr()
|
|
||||||
diags = append(diags, exprDiags...)
|
|
||||||
contentExprs = append(contentExprs, expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(contentExprs) == 0 {
|
|
||||||
contentExprs = append(contentExprs, &LiteralValueExpr{
|
|
||||||
Val: cty.StringVal(""),
|
|
||||||
SrcRange: hcl.Range{
|
|
||||||
Filename: openFor.SrcRange.Filename,
|
|
||||||
Start: openFor.SrcRange.End,
|
|
||||||
End: openFor.SrcRange.End,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
contentExpr := &TemplateExpr{
|
|
||||||
Parts: contentExprs,
|
|
||||||
SrcRange: hcl.RangeBetween(contentExprs[0].Range(), contentExprs[len(contentExprs)-1].Range()),
|
|
||||||
}
|
|
||||||
|
|
||||||
forExpr := &ForExpr{
|
|
||||||
KeyVar: openFor.KeyVar,
|
|
||||||
ValVar: openFor.ValVar,
|
|
||||||
|
|
||||||
CollExpr: openFor.CollExpr,
|
|
||||||
ValExpr: contentExpr,
|
|
||||||
|
|
||||||
SrcRange: hcl.RangeBetween(openFor.SrcRange, endforRange),
|
|
||||||
OpenRange: openFor.SrcRange,
|
|
||||||
CloseRange: endforRange,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &TemplateJoinExpr{
|
|
||||||
Tuple: forExpr,
|
|
||||||
}, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *templateParser) Peek() templateToken {
|
|
||||||
return p.Tokens[p.pos]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *templateParser) Read() templateToken {
|
|
||||||
ret := p.Peek()
|
|
||||||
if _, end := ret.(*templateEndToken); !end {
|
|
||||||
p.pos++
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseTemplateParts produces a flat sequence of "template tokens", which are
|
|
||||||
// either literal values (with any "trimming" already applied), interpolation
|
|
||||||
// sequences, or control flow markers.
|
|
||||||
//
|
|
||||||
// A further pass is required on the result to turn it into an AST.
|
|
||||||
func (p *parser) parseTemplateParts(end TokenType) (*templateParts, hcl.Diagnostics) {
|
|
||||||
var parts []templateToken
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
|
|
||||||
startRange := p.NextRange()
|
|
||||||
ltrimNext := false
|
|
||||||
nextCanTrimPrev := false
|
|
||||||
var endRange hcl.Range
|
|
||||||
|
|
||||||
Token:
|
|
||||||
for {
|
|
||||||
next := p.Read()
|
|
||||||
if next.Type == end {
|
|
||||||
// all done!
|
|
||||||
endRange = next.Range
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
ltrim := ltrimNext
|
|
||||||
ltrimNext = false
|
|
||||||
canTrimPrev := nextCanTrimPrev
|
|
||||||
nextCanTrimPrev = false
|
|
||||||
|
|
||||||
switch next.Type {
|
|
||||||
case TokenStringLit, TokenQuotedLit:
|
|
||||||
str, strDiags := p.decodeStringLit(next)
|
|
||||||
diags = append(diags, strDiags...)
|
|
||||||
|
|
||||||
if ltrim {
|
|
||||||
str = strings.TrimLeftFunc(str, unicode.IsSpace)
|
|
||||||
}
|
|
||||||
|
|
||||||
parts = append(parts, &templateLiteralToken{
|
|
||||||
Val: str,
|
|
||||||
SrcRange: next.Range,
|
|
||||||
})
|
|
||||||
nextCanTrimPrev = true
|
|
||||||
|
|
||||||
case TokenTemplateInterp:
|
|
||||||
// if the opener is ${~ then we want to eat any trailing whitespace
|
|
||||||
// in the preceding literal token, assuming it is indeed a literal
|
|
||||||
// token.
|
|
||||||
if canTrimPrev && len(next.Bytes) == 3 && next.Bytes[2] == '~' && len(parts) > 0 {
|
|
||||||
prevExpr := parts[len(parts)-1]
|
|
||||||
if lexpr, ok := prevExpr.(*templateLiteralToken); ok {
|
|
||||||
lexpr.Val = strings.TrimRightFunc(lexpr.Val, unicode.IsSpace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.PushIncludeNewlines(false)
|
|
||||||
expr, exprDiags := p.ParseExpression()
|
|
||||||
diags = append(diags, exprDiags...)
|
|
||||||
close := p.Peek()
|
|
||||||
if close.Type != TokenTemplateSeqEnd {
|
|
||||||
if !p.recovery {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Extra characters after interpolation expression",
|
|
||||||
Detail: "Expected a closing brace to end the interpolation expression, but found extra characters.",
|
|
||||||
Subject: &close.Range,
|
|
||||||
Context: hcl.RangeBetween(startRange, close.Range).Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p.recover(TokenTemplateSeqEnd)
|
|
||||||
} else {
|
|
||||||
p.Read() // eat closing brace
|
|
||||||
|
|
||||||
// If the closer is ~} then we want to eat any leading
|
|
||||||
// whitespace on the next token, if it turns out to be a
|
|
||||||
// literal token.
|
|
||||||
if len(close.Bytes) == 2 && close.Bytes[0] == '~' {
|
|
||||||
ltrimNext = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.PopIncludeNewlines()
|
|
||||||
parts = append(parts, &templateInterpToken{
|
|
||||||
Expr: expr,
|
|
||||||
SrcRange: hcl.RangeBetween(next.Range, close.Range),
|
|
||||||
})
|
|
||||||
|
|
||||||
case TokenTemplateControl:
|
|
||||||
// if the opener is %{~ then we want to eat any trailing whitespace
|
|
||||||
// in the preceding literal token, assuming it is indeed a literal
|
|
||||||
// token.
|
|
||||||
if canTrimPrev && len(next.Bytes) == 3 && next.Bytes[2] == '~' && len(parts) > 0 {
|
|
||||||
prevExpr := parts[len(parts)-1]
|
|
||||||
if lexpr, ok := prevExpr.(*templateLiteralToken); ok {
|
|
||||||
lexpr.Val = strings.TrimRightFunc(lexpr.Val, unicode.IsSpace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.PushIncludeNewlines(false)
|
|
||||||
|
|
||||||
kw := p.Peek()
|
|
||||||
if kw.Type != TokenIdent {
|
|
||||||
if !p.recovery {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid template directive",
|
|
||||||
Detail: "A template directive keyword (\"if\", \"for\", etc) is expected at the beginning of a %{ sequence.",
|
|
||||||
Subject: &kw.Range,
|
|
||||||
Context: hcl.RangeBetween(next.Range, kw.Range).Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p.recover(TokenTemplateSeqEnd)
|
|
||||||
p.PopIncludeNewlines()
|
|
||||||
continue Token
|
|
||||||
}
|
|
||||||
p.Read() // eat keyword token
|
|
||||||
|
|
||||||
switch {
|
|
||||||
|
|
||||||
case ifKeyword.TokenMatches(kw):
|
|
||||||
condExpr, exprDiags := p.ParseExpression()
|
|
||||||
diags = append(diags, exprDiags...)
|
|
||||||
parts = append(parts, &templateIfToken{
|
|
||||||
CondExpr: condExpr,
|
|
||||||
SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
|
|
||||||
})
|
|
||||||
|
|
||||||
case elseKeyword.TokenMatches(kw):
|
|
||||||
parts = append(parts, &templateEndCtrlToken{
|
|
||||||
Type: templateElse,
|
|
||||||
SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
|
|
||||||
})
|
|
||||||
|
|
||||||
case endifKeyword.TokenMatches(kw):
|
|
||||||
parts = append(parts, &templateEndCtrlToken{
|
|
||||||
Type: templateEndIf,
|
|
||||||
SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
|
|
||||||
})
|
|
||||||
|
|
||||||
case forKeyword.TokenMatches(kw):
|
|
||||||
var keyName, valName string
|
|
||||||
if p.Peek().Type != TokenIdent {
|
|
||||||
if !p.recovery {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid 'for' directive",
|
|
||||||
Detail: "For directive requires variable name after 'for'.",
|
|
||||||
Subject: p.Peek().Range.Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p.recover(TokenTemplateSeqEnd)
|
|
||||||
p.PopIncludeNewlines()
|
|
||||||
continue Token
|
|
||||||
}
|
|
||||||
|
|
||||||
valName = string(p.Read().Bytes)
|
|
||||||
|
|
||||||
if p.Peek().Type == TokenComma {
|
|
||||||
// What we just read was actually the key, then.
|
|
||||||
keyName = valName
|
|
||||||
p.Read() // eat comma
|
|
||||||
|
|
||||||
if p.Peek().Type != TokenIdent {
|
|
||||||
if !p.recovery {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid 'for' directive",
|
|
||||||
Detail: "For directive requires value variable name after comma.",
|
|
||||||
Subject: p.Peek().Range.Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p.recover(TokenTemplateSeqEnd)
|
|
||||||
p.PopIncludeNewlines()
|
|
||||||
continue Token
|
|
||||||
}
|
|
||||||
|
|
||||||
valName = string(p.Read().Bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !inKeyword.TokenMatches(p.Peek()) {
|
|
||||||
if !p.recovery {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid 'for' directive",
|
|
||||||
Detail: "For directive requires 'in' keyword after names.",
|
|
||||||
Subject: p.Peek().Range.Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p.recover(TokenTemplateSeqEnd)
|
|
||||||
p.PopIncludeNewlines()
|
|
||||||
continue Token
|
|
||||||
}
|
|
||||||
p.Read() // eat 'in' keyword
|
|
||||||
|
|
||||||
collExpr, collDiags := p.ParseExpression()
|
|
||||||
diags = append(diags, collDiags...)
|
|
||||||
parts = append(parts, &templateForToken{
|
|
||||||
KeyVar: keyName,
|
|
||||||
ValVar: valName,
|
|
||||||
CollExpr: collExpr,
|
|
||||||
|
|
||||||
SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
|
|
||||||
})
|
|
||||||
|
|
||||||
case endforKeyword.TokenMatches(kw):
|
|
||||||
parts = append(parts, &templateEndCtrlToken{
|
|
||||||
Type: templateEndFor,
|
|
||||||
SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
|
|
||||||
})
|
|
||||||
|
|
||||||
default:
|
|
||||||
if !p.recovery {
|
|
||||||
suggestions := []string{"if", "for", "else", "endif", "endfor"}
|
|
||||||
given := string(kw.Bytes)
|
|
||||||
suggestion := nameSuggestion(given, suggestions)
|
|
||||||
if suggestion != "" {
|
|
||||||
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
|
||||||
}
|
|
||||||
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid template control keyword",
|
|
||||||
Detail: fmt.Sprintf("%q is not a valid template control keyword.%s", given, suggestion),
|
|
||||||
Subject: &kw.Range,
|
|
||||||
Context: hcl.RangeBetween(next.Range, kw.Range).Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p.recover(TokenTemplateSeqEnd)
|
|
||||||
p.PopIncludeNewlines()
|
|
||||||
continue Token
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
close := p.Peek()
|
|
||||||
if close.Type != TokenTemplateSeqEnd {
|
|
||||||
if !p.recovery {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: fmt.Sprintf("Extra characters in %s marker", kw.Bytes),
|
|
||||||
Detail: "Expected a closing brace to end the sequence, but found extra characters.",
|
|
||||||
Subject: &close.Range,
|
|
||||||
Context: hcl.RangeBetween(startRange, close.Range).Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p.recover(TokenTemplateSeqEnd)
|
|
||||||
} else {
|
|
||||||
p.Read() // eat closing brace
|
|
||||||
|
|
||||||
// If the closer is ~} then we want to eat any leading
|
|
||||||
// whitespace on the next token, if it turns out to be a
|
|
||||||
// literal token.
|
|
||||||
if len(close.Bytes) == 2 && close.Bytes[0] == '~' {
|
|
||||||
ltrimNext = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.PopIncludeNewlines()
|
|
||||||
|
|
||||||
default:
|
|
||||||
if !p.recovery {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unterminated template string",
|
|
||||||
Detail: "No closing marker was found for the string.",
|
|
||||||
Subject: &next.Range,
|
|
||||||
Context: hcl.RangeBetween(startRange, next.Range).Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
final := p.recover(end)
|
|
||||||
endRange = final.Range
|
|
||||||
break Token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(parts) == 0 {
|
|
||||||
// If a sequence has no content, we'll treat it as if it had an
|
|
||||||
// empty string in it because that's what the user probably means
|
|
||||||
// if they write "" in configuration.
|
|
||||||
parts = append(parts, &templateLiteralToken{
|
|
||||||
Val: "",
|
|
||||||
SrcRange: hcl.Range{
|
|
||||||
// Range is the zero-character span immediately after the
|
|
||||||
// opening quote.
|
|
||||||
Filename: startRange.Filename,
|
|
||||||
Start: startRange.End,
|
|
||||||
End: startRange.End,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always end with an end token, so the parser can produce diagnostics
|
|
||||||
// about unclosed items with proper position information.
|
|
||||||
parts = append(parts, &templateEndToken{
|
|
||||||
SrcRange: endRange,
|
|
||||||
})
|
|
||||||
|
|
||||||
ret := &templateParts{
|
|
||||||
Tokens: parts,
|
|
||||||
SrcRange: hcl.RangeBetween(startRange, endRange),
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// flushHeredocTemplateParts modifies in-place the line-leading literal strings
|
|
||||||
// to apply the flush heredoc processing rule: find the line with the smallest
|
|
||||||
// number of whitespace characters as prefix and then trim that number of
|
|
||||||
// characters from all of the lines.
|
|
||||||
//
|
|
||||||
// This rule is applied to static tokens rather than to the rendered result,
|
|
||||||
// so interpolating a string with leading whitespace cannot affect the chosen
|
|
||||||
// prefix length.
|
|
||||||
func flushHeredocTemplateParts(parts *templateParts) {
|
|
||||||
if len(parts.Tokens) == 0 {
|
|
||||||
// Nothing to do
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxInt = int((^uint(0)) >> 1)
|
|
||||||
|
|
||||||
minSpaces := maxInt
|
|
||||||
newline := true
|
|
||||||
var adjust []*templateLiteralToken
|
|
||||||
for _, ttok := range parts.Tokens {
|
|
||||||
if newline {
|
|
||||||
newline = false
|
|
||||||
var spaces int
|
|
||||||
if lit, ok := ttok.(*templateLiteralToken); ok {
|
|
||||||
orig := lit.Val
|
|
||||||
trimmed := strings.TrimLeftFunc(orig, unicode.IsSpace)
|
|
||||||
// If a token is entirely spaces and ends with a newline
|
|
||||||
// then it's a "blank line" and thus not considered for
|
|
||||||
// space-prefix-counting purposes.
|
|
||||||
if len(trimmed) == 0 && strings.HasSuffix(orig, "\n") {
|
|
||||||
spaces = maxInt
|
|
||||||
} else {
|
|
||||||
spaceBytes := len(lit.Val) - len(trimmed)
|
|
||||||
spaces, _ = textseg.TokenCount([]byte(orig[:spaceBytes]), textseg.ScanGraphemeClusters)
|
|
||||||
adjust = append(adjust, lit)
|
|
||||||
}
|
|
||||||
} else if _, ok := ttok.(*templateEndToken); ok {
|
|
||||||
break // don't process the end token since it never has spaces before it
|
|
||||||
}
|
|
||||||
if spaces < minSpaces {
|
|
||||||
minSpaces = spaces
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if lit, ok := ttok.(*templateLiteralToken); ok {
|
|
||||||
if strings.HasSuffix(lit.Val, "\n") {
|
|
||||||
newline = true // The following token, if any, begins a new line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, lit := range adjust {
|
|
||||||
// Since we want to count space _characters_ rather than space _bytes_,
|
|
||||||
// we can't just do a straightforward slice operation here and instead
|
|
||||||
// need to hunt for the split point with a scanner.
|
|
||||||
valBytes := []byte(lit.Val)
|
|
||||||
spaceByteCount := 0
|
|
||||||
for i := 0; i < minSpaces; i++ {
|
|
||||||
adv, _, _ := textseg.ScanGraphemeClusters(valBytes, true)
|
|
||||||
spaceByteCount += adv
|
|
||||||
valBytes = valBytes[adv:]
|
|
||||||
}
|
|
||||||
lit.Val = lit.Val[spaceByteCount:]
|
|
||||||
lit.SrcRange.Start.Column += minSpaces
|
|
||||||
lit.SrcRange.Start.Byte += spaceByteCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateParts struct {
|
|
||||||
Tokens []templateToken
|
|
||||||
SrcRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
// templateToken is a higher-level token that represents a single atom within
|
|
||||||
// the template language. Our template parsing first raises the raw token
|
|
||||||
// stream to a sequence of templateToken, and then transforms the result into
|
|
||||||
// an expression tree.
|
|
||||||
type templateToken interface {
|
|
||||||
templateToken() templateToken
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateLiteralToken struct {
|
|
||||||
Val string
|
|
||||||
SrcRange hcl.Range
|
|
||||||
isTemplateToken
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateInterpToken struct {
|
|
||||||
Expr Expression
|
|
||||||
SrcRange hcl.Range
|
|
||||||
isTemplateToken
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateIfToken struct {
|
|
||||||
CondExpr Expression
|
|
||||||
SrcRange hcl.Range
|
|
||||||
isTemplateToken
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateForToken struct {
|
|
||||||
KeyVar string // empty if ignoring key
|
|
||||||
ValVar string
|
|
||||||
CollExpr Expression
|
|
||||||
SrcRange hcl.Range
|
|
||||||
isTemplateToken
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateEndCtrlType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
templateEndIf templateEndCtrlType = iota
|
|
||||||
templateElse
|
|
||||||
templateEndFor
|
|
||||||
)
|
|
||||||
|
|
||||||
type templateEndCtrlToken struct {
|
|
||||||
Type templateEndCtrlType
|
|
||||||
SrcRange hcl.Range
|
|
||||||
isTemplateToken
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *templateEndCtrlToken) Name() string {
|
|
||||||
switch t.Type {
|
|
||||||
case templateEndIf:
|
|
||||||
return "endif"
|
|
||||||
case templateElse:
|
|
||||||
return "else"
|
|
||||||
case templateEndFor:
|
|
||||||
return "endfor"
|
|
||||||
default:
|
|
||||||
// should never happen
|
|
||||||
panic("invalid templateEndCtrlType")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateEndToken struct {
|
|
||||||
SrcRange hcl.Range
|
|
||||||
isTemplateToken
|
|
||||||
}
|
|
||||||
|
|
||||||
type isTemplateToken [0]int
|
|
||||||
|
|
||||||
func (t isTemplateToken) templateToken() templateToken {
|
|
||||||
return t
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseTraversalAbs parses an absolute traversal that is assumed to consume
|
|
||||||
// all of the remaining tokens in the peeker. The usual parser recovery
|
|
||||||
// behavior is not supported here because traversals are not expected to
|
|
||||||
// be parsed as part of a larger program.
|
|
||||||
func (p *parser) ParseTraversalAbs() (hcl.Traversal, hcl.Diagnostics) {
|
|
||||||
var ret hcl.Traversal
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
|
|
||||||
// Absolute traversal must always begin with a variable name
|
|
||||||
varTok := p.Read()
|
|
||||||
if varTok.Type != TokenIdent {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Variable name required",
|
|
||||||
Detail: "Must begin with a variable name.",
|
|
||||||
Subject: &varTok.Range,
|
|
||||||
})
|
|
||||||
return ret, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
varName := string(varTok.Bytes)
|
|
||||||
ret = append(ret, hcl.TraverseRoot{
|
|
||||||
Name: varName,
|
|
||||||
SrcRange: varTok.Range,
|
|
||||||
})
|
|
||||||
|
|
||||||
for {
|
|
||||||
next := p.Peek()
|
|
||||||
|
|
||||||
if next.Type == TokenEOF {
|
|
||||||
return ret, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
switch next.Type {
|
|
||||||
case TokenDot:
|
|
||||||
// Attribute access
|
|
||||||
dot := p.Read() // eat dot
|
|
||||||
nameTok := p.Read()
|
|
||||||
if nameTok.Type != TokenIdent {
|
|
||||||
if nameTok.Type == TokenStar {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Attribute name required",
|
|
||||||
Detail: "Splat expressions (.*) may not be used here.",
|
|
||||||
Subject: &nameTok.Range,
|
|
||||||
Context: hcl.RangeBetween(varTok.Range, nameTok.Range).Ptr(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Attribute name required",
|
|
||||||
Detail: "Dot must be followed by attribute name.",
|
|
||||||
Subject: &nameTok.Range,
|
|
||||||
Context: hcl.RangeBetween(varTok.Range, nameTok.Range).Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return ret, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
attrName := string(nameTok.Bytes)
|
|
||||||
ret = append(ret, hcl.TraverseAttr{
|
|
||||||
Name: attrName,
|
|
||||||
SrcRange: hcl.RangeBetween(dot.Range, nameTok.Range),
|
|
||||||
})
|
|
||||||
case TokenOBrack:
|
|
||||||
// Index
|
|
||||||
open := p.Read() // eat open bracket
|
|
||||||
next := p.Peek()
|
|
||||||
|
|
||||||
switch next.Type {
|
|
||||||
case TokenNumberLit:
|
|
||||||
tok := p.Read() // eat number
|
|
||||||
numVal, numDiags := p.numberLitValue(tok)
|
|
||||||
diags = append(diags, numDiags...)
|
|
||||||
|
|
||||||
close := p.Read()
|
|
||||||
if close.Type != TokenCBrack {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unclosed index brackets",
|
|
||||||
Detail: "Index key must be followed by a closing bracket.",
|
|
||||||
Subject: &close.Range,
|
|
||||||
Context: hcl.RangeBetween(open.Range, close.Range).Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = append(ret, hcl.TraverseIndex{
|
|
||||||
Key: numVal,
|
|
||||||
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
||||||
})
|
|
||||||
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return ret, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
case TokenOQuote:
|
|
||||||
str, _, strDiags := p.parseQuotedStringLiteral()
|
|
||||||
diags = append(diags, strDiags...)
|
|
||||||
|
|
||||||
close := p.Read()
|
|
||||||
if close.Type != TokenCBrack {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unclosed index brackets",
|
|
||||||
Detail: "Index key must be followed by a closing bracket.",
|
|
||||||
Subject: &close.Range,
|
|
||||||
Context: hcl.RangeBetween(open.Range, close.Range).Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = append(ret, hcl.TraverseIndex{
|
|
||||||
Key: cty.StringVal(str),
|
|
||||||
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
||||||
})
|
|
||||||
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return ret, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
if next.Type == TokenStar {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Attribute name required",
|
|
||||||
Detail: "Splat expressions ([*]) may not be used here.",
|
|
||||||
Subject: &next.Range,
|
|
||||||
Context: hcl.RangeBetween(varTok.Range, next.Range).Ptr(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Index value required",
|
|
||||||
Detail: "Index brackets must contain either a literal number or a literal string.",
|
|
||||||
Subject: &next.Range,
|
|
||||||
Context: hcl.RangeBetween(varTok.Range, next.Range).Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return ret, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid character",
|
|
||||||
Detail: "Expected an attribute access or an index operator.",
|
|
||||||
Subject: &next.Range,
|
|
||||||
Context: hcl.RangeBetween(varTok.Range, next.Range).Ptr(),
|
|
||||||
})
|
|
||||||
return ret, diags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,212 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is set to true at init() time in tests, to enable more useful output
|
|
||||||
// if a stack discipline error is detected. It should not be enabled in
|
|
||||||
// normal mode since there is a performance penalty from accessing the
|
|
||||||
// runtime stack to produce the traces, but could be temporarily set to
|
|
||||||
// true for debugging if desired.
|
|
||||||
var tracePeekerNewlinesStack = false
|
|
||||||
|
|
||||||
type peeker struct {
|
|
||||||
Tokens Tokens
|
|
||||||
NextIndex int
|
|
||||||
|
|
||||||
IncludeComments bool
|
|
||||||
IncludeNewlinesStack []bool
|
|
||||||
|
|
||||||
// used only when tracePeekerNewlinesStack is set
|
|
||||||
newlineStackChanges []peekerNewlineStackChange
|
|
||||||
}
|
|
||||||
|
|
||||||
// for use in debugging the stack usage only
|
|
||||||
type peekerNewlineStackChange struct {
|
|
||||||
Pushing bool // if false, then popping
|
|
||||||
Frame runtime.Frame
|
|
||||||
Include bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPeeker(tokens Tokens, includeComments bool) *peeker {
|
|
||||||
return &peeker{
|
|
||||||
Tokens: tokens,
|
|
||||||
IncludeComments: includeComments,
|
|
||||||
|
|
||||||
IncludeNewlinesStack: []bool{true},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peeker) Peek() Token {
|
|
||||||
ret, _ := p.nextToken()
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peeker) Read() Token {
|
|
||||||
ret, nextIdx := p.nextToken()
|
|
||||||
p.NextIndex = nextIdx
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peeker) NextRange() hcl.Range {
|
|
||||||
return p.Peek().Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peeker) PrevRange() hcl.Range {
|
|
||||||
if p.NextIndex == 0 {
|
|
||||||
return p.NextRange()
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.Tokens[p.NextIndex-1].Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peeker) nextToken() (Token, int) {
|
|
||||||
for i := p.NextIndex; i < len(p.Tokens); i++ {
|
|
||||||
tok := p.Tokens[i]
|
|
||||||
switch tok.Type {
|
|
||||||
case TokenComment:
|
|
||||||
if !p.IncludeComments {
|
|
||||||
// Single-line comment tokens, starting with # or //, absorb
|
|
||||||
// the trailing newline that terminates them as part of their
|
|
||||||
// bytes. When we're filtering out comments, we must as a
|
|
||||||
// special case transform these to newline tokens in order
|
|
||||||
// to properly parse newline-terminated block items.
|
|
||||||
|
|
||||||
if p.includingNewlines() {
|
|
||||||
if len(tok.Bytes) > 0 && tok.Bytes[len(tok.Bytes)-1] == '\n' {
|
|
||||||
fakeNewline := Token{
|
|
||||||
Type: TokenNewline,
|
|
||||||
Bytes: tok.Bytes[len(tok.Bytes)-1 : len(tok.Bytes)],
|
|
||||||
|
|
||||||
// We use the whole token range as the newline
|
|
||||||
// range, even though that's a little... weird,
|
|
||||||
// because otherwise we'd need to go count
|
|
||||||
// characters again in order to figure out the
|
|
||||||
// column of the newline, and that complexity
|
|
||||||
// isn't justified when ranges of newlines are
|
|
||||||
// so rarely printed anyway.
|
|
||||||
Range: tok.Range,
|
|
||||||
}
|
|
||||||
return fakeNewline, i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
case TokenNewline:
|
|
||||||
if !p.includingNewlines() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tok, i + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we fall out here then we'll return the EOF token, and leave
|
|
||||||
// our index pointed off the end of the array so we'll keep
|
|
||||||
// returning EOF in future too.
|
|
||||||
return p.Tokens[len(p.Tokens)-1], len(p.Tokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peeker) includingNewlines() bool {
|
|
||||||
return p.IncludeNewlinesStack[len(p.IncludeNewlinesStack)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peeker) PushIncludeNewlines(include bool) {
|
|
||||||
if tracePeekerNewlinesStack {
|
|
||||||
// Record who called us so that we can more easily track down any
|
|
||||||
// mismanagement of the stack in the parser.
|
|
||||||
callers := []uintptr{0}
|
|
||||||
runtime.Callers(2, callers)
|
|
||||||
frames := runtime.CallersFrames(callers)
|
|
||||||
frame, _ := frames.Next()
|
|
||||||
p.newlineStackChanges = append(p.newlineStackChanges, peekerNewlineStackChange{
|
|
||||||
true, frame, include,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
p.IncludeNewlinesStack = append(p.IncludeNewlinesStack, include)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peeker) PopIncludeNewlines() bool {
|
|
||||||
stack := p.IncludeNewlinesStack
|
|
||||||
remain, ret := stack[:len(stack)-1], stack[len(stack)-1]
|
|
||||||
p.IncludeNewlinesStack = remain
|
|
||||||
|
|
||||||
if tracePeekerNewlinesStack {
|
|
||||||
// Record who called us so that we can more easily track down any
|
|
||||||
// mismanagement of the stack in the parser.
|
|
||||||
callers := []uintptr{0}
|
|
||||||
runtime.Callers(2, callers)
|
|
||||||
frames := runtime.CallersFrames(callers)
|
|
||||||
frame, _ := frames.Next()
|
|
||||||
p.newlineStackChanges = append(p.newlineStackChanges, peekerNewlineStackChange{
|
|
||||||
false, frame, ret,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertEmptyNewlinesStack checks if the IncludeNewlinesStack is empty, doing
|
|
||||||
// panicking if it is not. This can be used to catch stack mismanagement that
|
|
||||||
// might otherwise just cause confusing downstream errors.
|
|
||||||
//
|
|
||||||
// This function is a no-op if the stack is empty when called.
|
|
||||||
//
|
|
||||||
// If newlines stack tracing is enabled by setting the global variable
|
|
||||||
// tracePeekerNewlinesStack at init time, a full log of all of the push/pop
|
|
||||||
// calls will be produced to help identify which caller in the parser is
|
|
||||||
// misbehaving.
|
|
||||||
func (p *peeker) AssertEmptyIncludeNewlinesStack() {
|
|
||||||
if len(p.IncludeNewlinesStack) != 1 {
|
|
||||||
// Should never happen; indicates mismanagement of the stack inside
|
|
||||||
// the parser.
|
|
||||||
if p.newlineStackChanges != nil { // only if traceNewlinesStack is enabled above
|
|
||||||
panic(fmt.Errorf(
|
|
||||||
"non-empty IncludeNewlinesStack after parse with %d calls unaccounted for:\n%s",
|
|
||||||
len(p.IncludeNewlinesStack)-1,
|
|
||||||
formatPeekerNewlineStackChanges(p.newlineStackChanges),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
panic(fmt.Errorf("non-empty IncludeNewlinesStack after parse: %#v", p.IncludeNewlinesStack))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatPeekerNewlineStackChanges(changes []peekerNewlineStackChange) string {
|
|
||||||
indent := 0
|
|
||||||
var buf bytes.Buffer
|
|
||||||
for _, change := range changes {
|
|
||||||
funcName := change.Frame.Function
|
|
||||||
if idx := strings.LastIndexByte(funcName, '.'); idx != -1 {
|
|
||||||
funcName = funcName[idx+1:]
|
|
||||||
}
|
|
||||||
filename := change.Frame.File
|
|
||||||
if idx := strings.LastIndexByte(filename, filepath.Separator); idx != -1 {
|
|
||||||
filename = filename[idx+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch change.Pushing {
|
|
||||||
|
|
||||||
case true:
|
|
||||||
buf.WriteString(strings.Repeat(" ", indent))
|
|
||||||
fmt.Fprintf(&buf, "PUSH %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line)
|
|
||||||
indent++
|
|
||||||
|
|
||||||
case false:
|
|
||||||
indent--
|
|
||||||
buf.WriteString(strings.Repeat(" ", indent))
|
|
||||||
fmt.Fprintf(&buf, "POP %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
|
@ -1,171 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseConfig parses the given buffer as a whole HCL config file, returning
|
|
||||||
// a *hcl.File representing its contents. If HasErrors called on the returned
|
|
||||||
// diagnostics returns true, the returned body is likely to be incomplete
|
|
||||||
// and should therefore be used with care.
|
|
||||||
//
|
|
||||||
// The body in the returned file has dynamic type *hclsyntax.Body, so callers
|
|
||||||
// may freely type-assert this to get access to the full hclsyntax API in
|
|
||||||
// situations where detailed access is required. However, most common use-cases
|
|
||||||
// should be served using the hcl.Body interface to ensure compatibility with
|
|
||||||
// other configurationg syntaxes, such as JSON.
|
|
||||||
func ParseConfig(src []byte, filename string, start hcl.Pos) (*hcl.File, hcl.Diagnostics) {
|
|
||||||
tokens, diags := LexConfig(src, filename, start)
|
|
||||||
peeker := newPeeker(tokens, false)
|
|
||||||
parser := &parser{peeker: peeker}
|
|
||||||
body, parseDiags := parser.ParseBody(TokenEOF)
|
|
||||||
diags = append(diags, parseDiags...)
|
|
||||||
|
|
||||||
// Panic if the parser uses incorrect stack discipline with the peeker's
|
|
||||||
// newlines stack, since otherwise it will produce confusing downstream
|
|
||||||
// errors.
|
|
||||||
peeker.AssertEmptyIncludeNewlinesStack()
|
|
||||||
|
|
||||||
return &hcl.File{
|
|
||||||
Body: body,
|
|
||||||
Bytes: src,
|
|
||||||
|
|
||||||
Nav: navigation{
|
|
||||||
root: body,
|
|
||||||
},
|
|
||||||
}, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseExpression parses the given buffer as a standalone HCL expression,
|
|
||||||
// returning it as an instance of Expression.
|
|
||||||
func ParseExpression(src []byte, filename string, start hcl.Pos) (Expression, hcl.Diagnostics) {
|
|
||||||
tokens, diags := LexExpression(src, filename, start)
|
|
||||||
peeker := newPeeker(tokens, false)
|
|
||||||
parser := &parser{peeker: peeker}
|
|
||||||
|
|
||||||
// Bare expressions are always parsed in "ignore newlines" mode, as if
|
|
||||||
// they were wrapped in parentheses.
|
|
||||||
parser.PushIncludeNewlines(false)
|
|
||||||
|
|
||||||
expr, parseDiags := parser.ParseExpression()
|
|
||||||
diags = append(diags, parseDiags...)
|
|
||||||
|
|
||||||
next := parser.Peek()
|
|
||||||
if next.Type != TokenEOF && !parser.recovery {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Extra characters after expression",
|
|
||||||
Detail: "An expression was successfully parsed, but extra characters were found after it.",
|
|
||||||
Subject: &next.Range,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
parser.PopIncludeNewlines()
|
|
||||||
|
|
||||||
// Panic if the parser uses incorrect stack discipline with the peeker's
|
|
||||||
// newlines stack, since otherwise it will produce confusing downstream
|
|
||||||
// errors.
|
|
||||||
peeker.AssertEmptyIncludeNewlinesStack()
|
|
||||||
|
|
||||||
return expr, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseTemplate parses the given buffer as a standalone HCL template,
|
|
||||||
// returning it as an instance of Expression.
|
|
||||||
func ParseTemplate(src []byte, filename string, start hcl.Pos) (Expression, hcl.Diagnostics) {
|
|
||||||
tokens, diags := LexTemplate(src, filename, start)
|
|
||||||
peeker := newPeeker(tokens, false)
|
|
||||||
parser := &parser{peeker: peeker}
|
|
||||||
expr, parseDiags := parser.ParseTemplate()
|
|
||||||
diags = append(diags, parseDiags...)
|
|
||||||
|
|
||||||
// Panic if the parser uses incorrect stack discipline with the peeker's
|
|
||||||
// newlines stack, since otherwise it will produce confusing downstream
|
|
||||||
// errors.
|
|
||||||
peeker.AssertEmptyIncludeNewlinesStack()
|
|
||||||
|
|
||||||
return expr, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseTraversalAbs parses the given buffer as a standalone absolute traversal.
|
|
||||||
//
|
|
||||||
// Parsing as a traversal is more limited than parsing as an expession since
|
|
||||||
// it allows only attribute and indexing operations on variables. Traverals
|
|
||||||
// are useful as a syntax for referring to objects without necessarily
|
|
||||||
// evaluating them.
|
|
||||||
func ParseTraversalAbs(src []byte, filename string, start hcl.Pos) (hcl.Traversal, hcl.Diagnostics) {
|
|
||||||
tokens, diags := LexExpression(src, filename, start)
|
|
||||||
peeker := newPeeker(tokens, false)
|
|
||||||
parser := &parser{peeker: peeker}
|
|
||||||
|
|
||||||
// Bare traverals are always parsed in "ignore newlines" mode, as if
|
|
||||||
// they were wrapped in parentheses.
|
|
||||||
parser.PushIncludeNewlines(false)
|
|
||||||
|
|
||||||
expr, parseDiags := parser.ParseTraversalAbs()
|
|
||||||
diags = append(diags, parseDiags...)
|
|
||||||
|
|
||||||
parser.PopIncludeNewlines()
|
|
||||||
|
|
||||||
// Panic if the parser uses incorrect stack discipline with the peeker's
|
|
||||||
// newlines stack, since otherwise it will produce confusing downstream
|
|
||||||
// errors.
|
|
||||||
peeker.AssertEmptyIncludeNewlinesStack()
|
|
||||||
|
|
||||||
return expr, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// LexConfig performs lexical analysis on the given buffer, treating it as a
|
|
||||||
// whole HCL config file, and returns the resulting tokens.
|
|
||||||
//
|
|
||||||
// Only minimal validation is done during lexical analysis, so the returned
|
|
||||||
// diagnostics may include errors about lexical issues such as bad character
|
|
||||||
// encodings or unrecognized characters, but full parsing is required to
|
|
||||||
// detect _all_ syntax errors.
|
|
||||||
func LexConfig(src []byte, filename string, start hcl.Pos) (Tokens, hcl.Diagnostics) {
|
|
||||||
tokens := scanTokens(src, filename, start, scanNormal)
|
|
||||||
diags := checkInvalidTokens(tokens)
|
|
||||||
return tokens, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// LexExpression performs lexical analysis on the given buffer, treating it as
|
|
||||||
// a standalone HCL expression, and returns the resulting tokens.
|
|
||||||
//
|
|
||||||
// Only minimal validation is done during lexical analysis, so the returned
|
|
||||||
// diagnostics may include errors about lexical issues such as bad character
|
|
||||||
// encodings or unrecognized characters, but full parsing is required to
|
|
||||||
// detect _all_ syntax errors.
|
|
||||||
func LexExpression(src []byte, filename string, start hcl.Pos) (Tokens, hcl.Diagnostics) {
|
|
||||||
// This is actually just the same thing as LexConfig, since configs
|
|
||||||
// and expressions lex in the same way.
|
|
||||||
tokens := scanTokens(src, filename, start, scanNormal)
|
|
||||||
diags := checkInvalidTokens(tokens)
|
|
||||||
return tokens, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// LexTemplate performs lexical analysis on the given buffer, treating it as a
|
|
||||||
// standalone HCL template, and returns the resulting tokens.
|
|
||||||
//
|
|
||||||
// Only minimal validation is done during lexical analysis, so the returned
|
|
||||||
// diagnostics may include errors about lexical issues such as bad character
|
|
||||||
// encodings or unrecognized characters, but full parsing is required to
|
|
||||||
// detect _all_ syntax errors.
|
|
||||||
func LexTemplate(src []byte, filename string, start hcl.Pos) (Tokens, hcl.Diagnostics) {
|
|
||||||
tokens := scanTokens(src, filename, start, scanTemplate)
|
|
||||||
diags := checkInvalidTokens(tokens)
|
|
||||||
return tokens, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidIdentifier tests if the given string could be a valid identifier in
|
|
||||||
// a native syntax expression.
|
|
||||||
//
|
|
||||||
// This is useful when accepting names from the user that will be used as
|
|
||||||
// variable or attribute names in the scope, to ensure that any name chosen
|
|
||||||
// will be traversable using the variable or attribute traversal syntax.
|
|
||||||
func ValidIdentifier(s string) bool {
|
|
||||||
// This is a kinda-expensive way to do something pretty simple, but it
|
|
||||||
// is easiest to do with our existing scanner-related infrastructure here
|
|
||||||
// and nobody should be validating identifiers in a tight loop.
|
|
||||||
tokens := scanTokens([]byte(s), "", hcl.Pos{}, scanIdentOnly)
|
|
||||||
return len(tokens) == 2 && tokens[0].Type == TokenIdent && tokens[1].Type == TokenEOF
|
|
||||||
}
|
|
|
@ -1,301 +0,0 @@
|
||||||
//line scan_string_lit.rl:1
|
|
||||||
|
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
// This file is generated from scan_string_lit.rl. DO NOT EDIT.
|
|
||||||
|
|
||||||
//line scan_string_lit.go:9
|
|
||||||
var _hclstrtok_actions []byte = []byte{
|
|
||||||
0, 1, 0, 1, 1, 2, 1, 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
var _hclstrtok_key_offsets []byte = []byte{
|
|
||||||
0, 0, 2, 4, 6, 10, 14, 18,
|
|
||||||
22, 27, 31, 36, 41, 46, 51, 57,
|
|
||||||
62, 74, 85, 96, 107, 118, 129, 140,
|
|
||||||
151,
|
|
||||||
}
|
|
||||||
|
|
||||||
var _hclstrtok_trans_keys []byte = []byte{
|
|
||||||
128, 191, 128, 191, 128, 191, 10, 13,
|
|
||||||
36, 37, 10, 13, 36, 37, 10, 13,
|
|
||||||
36, 37, 10, 13, 36, 37, 10, 13,
|
|
||||||
36, 37, 123, 10, 13, 36, 37, 10,
|
|
||||||
13, 36, 37, 92, 10, 13, 36, 37,
|
|
||||||
92, 10, 13, 36, 37, 92, 10, 13,
|
|
||||||
36, 37, 92, 10, 13, 36, 37, 92,
|
|
||||||
123, 10, 13, 36, 37, 92, 85, 117,
|
|
||||||
128, 191, 192, 223, 224, 239, 240, 247,
|
|
||||||
248, 255, 10, 13, 36, 37, 92, 48,
|
|
||||||
57, 65, 70, 97, 102, 10, 13, 36,
|
|
||||||
37, 92, 48, 57, 65, 70, 97, 102,
|
|
||||||
10, 13, 36, 37, 92, 48, 57, 65,
|
|
||||||
70, 97, 102, 10, 13, 36, 37, 92,
|
|
||||||
48, 57, 65, 70, 97, 102, 10, 13,
|
|
||||||
36, 37, 92, 48, 57, 65, 70, 97,
|
|
||||||
102, 10, 13, 36, 37, 92, 48, 57,
|
|
||||||
65, 70, 97, 102, 10, 13, 36, 37,
|
|
||||||
92, 48, 57, 65, 70, 97, 102, 10,
|
|
||||||
13, 36, 37, 92, 48, 57, 65, 70,
|
|
||||||
97, 102,
|
|
||||||
}
|
|
||||||
|
|
||||||
var _hclstrtok_single_lengths []byte = []byte{
|
|
||||||
0, 0, 0, 0, 4, 4, 4, 4,
|
|
||||||
5, 4, 5, 5, 5, 5, 6, 5,
|
|
||||||
2, 5, 5, 5, 5, 5, 5, 5,
|
|
||||||
5,
|
|
||||||
}
|
|
||||||
|
|
||||||
var _hclstrtok_range_lengths []byte = []byte{
|
|
||||||
0, 1, 1, 1, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
5, 3, 3, 3, 3, 3, 3, 3,
|
|
||||||
3,
|
|
||||||
}
|
|
||||||
|
|
||||||
var _hclstrtok_index_offsets []byte = []byte{
|
|
||||||
0, 0, 2, 4, 6, 11, 16, 21,
|
|
||||||
26, 32, 37, 43, 49, 55, 61, 68,
|
|
||||||
74, 82, 91, 100, 109, 118, 127, 136,
|
|
||||||
145,
|
|
||||||
}
|
|
||||||
|
|
||||||
var _hclstrtok_indicies []byte = []byte{
|
|
||||||
0, 1, 2, 1, 3, 1, 5, 6,
|
|
||||||
7, 8, 4, 10, 11, 12, 13, 9,
|
|
||||||
14, 11, 12, 13, 9, 10, 11, 15,
|
|
||||||
13, 9, 10, 11, 12, 13, 14, 9,
|
|
||||||
10, 11, 12, 15, 9, 17, 18, 19,
|
|
||||||
20, 21, 16, 23, 24, 25, 26, 27,
|
|
||||||
22, 0, 24, 25, 26, 27, 22, 23,
|
|
||||||
24, 28, 26, 27, 22, 23, 24, 25,
|
|
||||||
26, 27, 0, 22, 23, 24, 25, 28,
|
|
||||||
27, 22, 29, 30, 22, 2, 3, 31,
|
|
||||||
22, 0, 23, 24, 25, 26, 27, 32,
|
|
||||||
32, 32, 22, 23, 24, 25, 26, 27,
|
|
||||||
33, 33, 33, 22, 23, 24, 25, 26,
|
|
||||||
27, 34, 34, 34, 22, 23, 24, 25,
|
|
||||||
26, 27, 30, 30, 30, 22, 23, 24,
|
|
||||||
25, 26, 27, 35, 35, 35, 22, 23,
|
|
||||||
24, 25, 26, 27, 36, 36, 36, 22,
|
|
||||||
23, 24, 25, 26, 27, 37, 37, 37,
|
|
||||||
22, 23, 24, 25, 26, 27, 0, 0,
|
|
||||||
0, 22,
|
|
||||||
}
|
|
||||||
|
|
||||||
var _hclstrtok_trans_targs []byte = []byte{
|
|
||||||
11, 0, 1, 2, 4, 5, 6, 7,
|
|
||||||
9, 4, 5, 6, 7, 9, 5, 8,
|
|
||||||
10, 11, 12, 13, 15, 16, 10, 11,
|
|
||||||
12, 13, 15, 16, 14, 17, 21, 3,
|
|
||||||
18, 19, 20, 22, 23, 24,
|
|
||||||
}
|
|
||||||
|
|
||||||
var _hclstrtok_trans_actions []byte = []byte{
|
|
||||||
0, 0, 0, 0, 0, 1, 1, 1,
|
|
||||||
1, 3, 5, 5, 5, 5, 0, 0,
|
|
||||||
0, 1, 1, 1, 1, 1, 3, 5,
|
|
||||||
5, 5, 5, 5, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
var _hclstrtok_eof_actions []byte = []byte{
|
|
||||||
0, 0, 0, 0, 0, 3, 3, 3,
|
|
||||||
3, 3, 0, 3, 3, 3, 3, 3,
|
|
||||||
3, 3, 3, 3, 3, 3, 3, 3,
|
|
||||||
3,
|
|
||||||
}
|
|
||||||
|
|
||||||
const hclstrtok_start int = 4
|
|
||||||
const hclstrtok_first_final int = 4
|
|
||||||
const hclstrtok_error int = 0
|
|
||||||
|
|
||||||
const hclstrtok_en_quoted int = 10
|
|
||||||
const hclstrtok_en_unquoted int = 4
|
|
||||||
|
|
||||||
//line scan_string_lit.rl:10
|
|
||||||
|
|
||||||
func scanStringLit(data []byte, quoted bool) [][]byte {
|
|
||||||
var ret [][]byte
|
|
||||||
|
|
||||||
//line scan_string_lit.rl:61
|
|
||||||
|
|
||||||
// Ragel state
|
|
||||||
p := 0 // "Pointer" into data
|
|
||||||
pe := len(data) // End-of-data "pointer"
|
|
||||||
ts := 0
|
|
||||||
te := 0
|
|
||||||
eof := pe
|
|
||||||
|
|
||||||
var cs int // current state
|
|
||||||
switch {
|
|
||||||
case quoted:
|
|
||||||
cs = hclstrtok_en_quoted
|
|
||||||
default:
|
|
||||||
cs = hclstrtok_en_unquoted
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make Go compiler happy
|
|
||||||
_ = ts
|
|
||||||
_ = eof
|
|
||||||
|
|
||||||
/*token := func () {
|
|
||||||
ret = append(ret, data[ts:te])
|
|
||||||
}*/
|
|
||||||
|
|
||||||
//line scan_string_lit.go:154
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
//line scan_string_lit.go:158
|
|
||||||
{
|
|
||||||
var _klen int
|
|
||||||
var _trans int
|
|
||||||
var _acts int
|
|
||||||
var _nacts uint
|
|
||||||
var _keys int
|
|
||||||
if p == pe {
|
|
||||||
goto _test_eof
|
|
||||||
}
|
|
||||||
if cs == 0 {
|
|
||||||
goto _out
|
|
||||||
}
|
|
||||||
_resume:
|
|
||||||
_keys = int(_hclstrtok_key_offsets[cs])
|
|
||||||
_trans = int(_hclstrtok_index_offsets[cs])
|
|
||||||
|
|
||||||
_klen = int(_hclstrtok_single_lengths[cs])
|
|
||||||
if _klen > 0 {
|
|
||||||
_lower := int(_keys)
|
|
||||||
var _mid int
|
|
||||||
_upper := int(_keys + _klen - 1)
|
|
||||||
for {
|
|
||||||
if _upper < _lower {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
_mid = _lower + ((_upper - _lower) >> 1)
|
|
||||||
switch {
|
|
||||||
case data[p] < _hclstrtok_trans_keys[_mid]:
|
|
||||||
_upper = _mid - 1
|
|
||||||
case data[p] > _hclstrtok_trans_keys[_mid]:
|
|
||||||
_lower = _mid + 1
|
|
||||||
default:
|
|
||||||
_trans += int(_mid - int(_keys))
|
|
||||||
goto _match
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_keys += _klen
|
|
||||||
_trans += _klen
|
|
||||||
}
|
|
||||||
|
|
||||||
_klen = int(_hclstrtok_range_lengths[cs])
|
|
||||||
if _klen > 0 {
|
|
||||||
_lower := int(_keys)
|
|
||||||
var _mid int
|
|
||||||
_upper := int(_keys + (_klen << 1) - 2)
|
|
||||||
for {
|
|
||||||
if _upper < _lower {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
_mid = _lower + (((_upper - _lower) >> 1) & ^1)
|
|
||||||
switch {
|
|
||||||
case data[p] < _hclstrtok_trans_keys[_mid]:
|
|
||||||
_upper = _mid - 2
|
|
||||||
case data[p] > _hclstrtok_trans_keys[_mid+1]:
|
|
||||||
_lower = _mid + 2
|
|
||||||
default:
|
|
||||||
_trans += int((_mid - int(_keys)) >> 1)
|
|
||||||
goto _match
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_trans += _klen
|
|
||||||
}
|
|
||||||
|
|
||||||
_match:
|
|
||||||
_trans = int(_hclstrtok_indicies[_trans])
|
|
||||||
cs = int(_hclstrtok_trans_targs[_trans])
|
|
||||||
|
|
||||||
if _hclstrtok_trans_actions[_trans] == 0 {
|
|
||||||
goto _again
|
|
||||||
}
|
|
||||||
|
|
||||||
_acts = int(_hclstrtok_trans_actions[_trans])
|
|
||||||
_nacts = uint(_hclstrtok_actions[_acts])
|
|
||||||
_acts++
|
|
||||||
for ; _nacts > 0; _nacts-- {
|
|
||||||
_acts++
|
|
||||||
switch _hclstrtok_actions[_acts-1] {
|
|
||||||
case 0:
|
|
||||||
//line scan_string_lit.rl:40
|
|
||||||
|
|
||||||
// If te is behind p then we've skipped over some literal
|
|
||||||
// characters which we must now return.
|
|
||||||
if te < p {
|
|
||||||
ret = append(ret, data[te:p])
|
|
||||||
}
|
|
||||||
ts = p
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
//line scan_string_lit.rl:48
|
|
||||||
|
|
||||||
te = p
|
|
||||||
ret = append(ret, data[ts:te])
|
|
||||||
|
|
||||||
//line scan_string_lit.go:253
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_again:
|
|
||||||
if cs == 0 {
|
|
||||||
goto _out
|
|
||||||
}
|
|
||||||
p++
|
|
||||||
if p != pe {
|
|
||||||
goto _resume
|
|
||||||
}
|
|
||||||
_test_eof:
|
|
||||||
{
|
|
||||||
}
|
|
||||||
if p == eof {
|
|
||||||
__acts := _hclstrtok_eof_actions[cs]
|
|
||||||
__nacts := uint(_hclstrtok_actions[__acts])
|
|
||||||
__acts++
|
|
||||||
for ; __nacts > 0; __nacts-- {
|
|
||||||
__acts++
|
|
||||||
switch _hclstrtok_actions[__acts-1] {
|
|
||||||
case 1:
|
|
||||||
//line scan_string_lit.rl:48
|
|
||||||
|
|
||||||
te = p
|
|
||||||
ret = append(ret, data[ts:te])
|
|
||||||
|
|
||||||
//line scan_string_lit.go:278
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_out:
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//line scan_string_lit.rl:89
|
|
||||||
|
|
||||||
if te < p {
|
|
||||||
// Collect any leftover literal characters at the end of the input
|
|
||||||
ret = append(ret, data[te:p])
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we fall out here without being in a final state then we've
|
|
||||||
// encountered something that the scanner can't match, which should
|
|
||||||
// be impossible (the scanner matches all bytes _somehow_) but we'll
|
|
||||||
// tolerate it and let the caller deal with it.
|
|
||||||
if cs < hclstrtok_first_final {
|
|
||||||
ret = append(ret, data[p:len(data)])
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
|
@ -1,105 +0,0 @@
|
||||||
|
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
// This file is generated from scan_string_lit.rl. DO NOT EDIT.
|
|
||||||
%%{
|
|
||||||
# (except you are actually in scan_string_lit.rl here, so edit away!)
|
|
||||||
|
|
||||||
machine hclstrtok;
|
|
||||||
write data;
|
|
||||||
}%%
|
|
||||||
|
|
||||||
func scanStringLit(data []byte, quoted bool) [][]byte {
|
|
||||||
var ret [][]byte
|
|
||||||
|
|
||||||
%%{
|
|
||||||
include UnicodeDerived "unicode_derived.rl";
|
|
||||||
|
|
||||||
UTF8Cont = 0x80 .. 0xBF;
|
|
||||||
AnyUTF8 = (
|
|
||||||
0x00..0x7F |
|
|
||||||
0xC0..0xDF . UTF8Cont |
|
|
||||||
0xE0..0xEF . UTF8Cont . UTF8Cont |
|
|
||||||
0xF0..0xF7 . UTF8Cont . UTF8Cont . UTF8Cont
|
|
||||||
);
|
|
||||||
BadUTF8 = any - AnyUTF8;
|
|
||||||
|
|
||||||
Hex = ('0'..'9' | 'a'..'f' | 'A'..'F');
|
|
||||||
|
|
||||||
# Our goal with this patterns is to capture user intent as best as
|
|
||||||
# possible, even if the input is invalid. The caller will then verify
|
|
||||||
# whether each token is valid and generate suitable error messages
|
|
||||||
# if not.
|
|
||||||
UnicodeEscapeShort = "\\u" . Hex{0,4};
|
|
||||||
UnicodeEscapeLong = "\\U" . Hex{0,8};
|
|
||||||
UnicodeEscape = (UnicodeEscapeShort | UnicodeEscapeLong);
|
|
||||||
SimpleEscape = "\\" . (AnyUTF8 - ('U'|'u'))?;
|
|
||||||
TemplateEscape = ("$" . ("$" . ("{"?))?) | ("%" . ("%" . ("{"?))?);
|
|
||||||
Newline = ("\r\n" | "\r" | "\n");
|
|
||||||
|
|
||||||
action Begin {
|
|
||||||
// If te is behind p then we've skipped over some literal
|
|
||||||
// characters which we must now return.
|
|
||||||
if te < p {
|
|
||||||
ret = append(ret, data[te:p])
|
|
||||||
}
|
|
||||||
ts = p;
|
|
||||||
}
|
|
||||||
action End {
|
|
||||||
te = p;
|
|
||||||
ret = append(ret, data[ts:te]);
|
|
||||||
}
|
|
||||||
|
|
||||||
QuotedToken = (UnicodeEscape | SimpleEscape | TemplateEscape | Newline) >Begin %End;
|
|
||||||
UnquotedToken = (TemplateEscape | Newline) >Begin %End;
|
|
||||||
QuotedLiteral = (any - ("\\" | "$" | "%" | "\r" | "\n"));
|
|
||||||
UnquotedLiteral = (any - ("$" | "%" | "\r" | "\n"));
|
|
||||||
|
|
||||||
quoted := (QuotedToken | QuotedLiteral)**;
|
|
||||||
unquoted := (UnquotedToken | UnquotedLiteral)**;
|
|
||||||
|
|
||||||
}%%
|
|
||||||
|
|
||||||
// Ragel state
|
|
||||||
p := 0 // "Pointer" into data
|
|
||||||
pe := len(data) // End-of-data "pointer"
|
|
||||||
ts := 0
|
|
||||||
te := 0
|
|
||||||
eof := pe
|
|
||||||
|
|
||||||
var cs int // current state
|
|
||||||
switch {
|
|
||||||
case quoted:
|
|
||||||
cs = hclstrtok_en_quoted
|
|
||||||
default:
|
|
||||||
cs = hclstrtok_en_unquoted
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make Go compiler happy
|
|
||||||
_ = ts
|
|
||||||
_ = eof
|
|
||||||
|
|
||||||
/*token := func () {
|
|
||||||
ret = append(ret, data[ts:te])
|
|
||||||
}*/
|
|
||||||
|
|
||||||
%%{
|
|
||||||
write init nocs;
|
|
||||||
write exec;
|
|
||||||
}%%
|
|
||||||
|
|
||||||
if te < p {
|
|
||||||
// Collect any leftover literal characters at the end of the input
|
|
||||||
ret = append(ret, data[te:p])
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we fall out here without being in a final state then we've
|
|
||||||
// encountered something that the scanner can't match, which should
|
|
||||||
// be impossible (the scanner matches all bytes _somehow_) but we'll
|
|
||||||
// tolerate it and let the caller deal with it.
|
|
||||||
if cs < hclstrtok_first_final {
|
|
||||||
ret = append(ret, data[p:len(data)])
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,395 +0,0 @@
|
||||||
|
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This file is generated from scan_tokens.rl. DO NOT EDIT.
|
|
||||||
%%{
|
|
||||||
# (except when you are actually in scan_tokens.rl here, so edit away!)
|
|
||||||
|
|
||||||
machine hcltok;
|
|
||||||
write data;
|
|
||||||
}%%
|
|
||||||
|
|
||||||
func scanTokens(data []byte, filename string, start hcl.Pos, mode scanMode) []Token {
|
|
||||||
stripData := stripUTF8BOM(data)
|
|
||||||
start.Byte += len(data) - len(stripData)
|
|
||||||
data = stripData
|
|
||||||
|
|
||||||
f := &tokenAccum{
|
|
||||||
Filename: filename,
|
|
||||||
Bytes: data,
|
|
||||||
Pos: start,
|
|
||||||
StartByte: start.Byte,
|
|
||||||
}
|
|
||||||
|
|
||||||
%%{
|
|
||||||
include UnicodeDerived "unicode_derived.rl";
|
|
||||||
|
|
||||||
UTF8Cont = 0x80 .. 0xBF;
|
|
||||||
AnyUTF8 = (
|
|
||||||
0x00..0x7F |
|
|
||||||
0xC0..0xDF . UTF8Cont |
|
|
||||||
0xE0..0xEF . UTF8Cont . UTF8Cont |
|
|
||||||
0xF0..0xF7 . UTF8Cont . UTF8Cont . UTF8Cont
|
|
||||||
);
|
|
||||||
BrokenUTF8 = any - AnyUTF8;
|
|
||||||
|
|
||||||
NumberLitContinue = (digit|'.'|('e'|'E') ('+'|'-')? digit);
|
|
||||||
NumberLit = digit ("" | (NumberLitContinue - '.') | (NumberLitContinue* (NumberLitContinue - '.')));
|
|
||||||
Ident = (ID_Start | '_') (ID_Continue | '-')*;
|
|
||||||
|
|
||||||
# Symbols that just represent themselves are handled as a single rule.
|
|
||||||
SelfToken = "[" | "]" | "(" | ")" | "." | "," | "*" | "/" | "%" | "+" | "-" | "=" | "<" | ">" | "!" | "?" | ":" | "\n" | "&" | "|" | "~" | "^" | ";" | "`" | "'";
|
|
||||||
|
|
||||||
EqualOp = "==";
|
|
||||||
NotEqual = "!=";
|
|
||||||
GreaterThanEqual = ">=";
|
|
||||||
LessThanEqual = "<=";
|
|
||||||
LogicalAnd = "&&";
|
|
||||||
LogicalOr = "||";
|
|
||||||
|
|
||||||
Ellipsis = "...";
|
|
||||||
FatArrow = "=>";
|
|
||||||
|
|
||||||
Newline = '\r' ? '\n';
|
|
||||||
EndOfLine = Newline;
|
|
||||||
|
|
||||||
BeginStringTmpl = '"';
|
|
||||||
BeginHeredocTmpl = '<<' ('-')? Ident Newline;
|
|
||||||
|
|
||||||
Comment = (
|
|
||||||
# The :>> operator in these is a "finish-guarded concatenation",
|
|
||||||
# which terminates the sequence on its left when it completes
|
|
||||||
# the sequence on its right.
|
|
||||||
# In the single-line comment cases this is allowing us to make
|
|
||||||
# the trailing EndOfLine optional while still having the overall
|
|
||||||
# pattern terminate. In the multi-line case it ensures that
|
|
||||||
# the first comment in the file ends at the first */, rather than
|
|
||||||
# gobbling up all of the "any*" until the _final_ */ in the file.
|
|
||||||
("#" (any - EndOfLine)* :>> EndOfLine?) |
|
|
||||||
("//" (any - EndOfLine)* :>> EndOfLine?) |
|
|
||||||
("/*" any* :>> "*/")
|
|
||||||
);
|
|
||||||
|
|
||||||
# Note: hclwrite assumes that only ASCII spaces appear between tokens,
|
|
||||||
# and uses this assumption to recreate the spaces between tokens by
|
|
||||||
# looking at byte offset differences. This means it will produce
|
|
||||||
# incorrect results in the presence of tabs, but that's acceptable
|
|
||||||
# because the canonical style (which hclwrite itself can impose
|
|
||||||
# automatically is to never use tabs).
|
|
||||||
Spaces = (' ' | 0x09)+;
|
|
||||||
|
|
||||||
action beginStringTemplate {
|
|
||||||
token(TokenOQuote);
|
|
||||||
fcall stringTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
action endStringTemplate {
|
|
||||||
token(TokenCQuote);
|
|
||||||
fret;
|
|
||||||
}
|
|
||||||
|
|
||||||
action beginHeredocTemplate {
|
|
||||||
token(TokenOHeredoc);
|
|
||||||
// the token is currently the whole heredoc introducer, like
|
|
||||||
// <<EOT or <<-EOT, followed by a newline. We want to extract
|
|
||||||
// just the "EOT" portion that we'll use as the closing marker.
|
|
||||||
|
|
||||||
marker := data[ts+2:te-1]
|
|
||||||
if marker[0] == '-' {
|
|
||||||
marker = marker[1:]
|
|
||||||
}
|
|
||||||
if marker[len(marker)-1] == '\r' {
|
|
||||||
marker = marker[:len(marker)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
heredocs = append(heredocs, heredocInProgress{
|
|
||||||
Marker: marker,
|
|
||||||
StartOfLine: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
fcall heredocTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
action heredocLiteralEOL {
|
|
||||||
// This action is called specificially when a heredoc literal
|
|
||||||
// ends with a newline character.
|
|
||||||
|
|
||||||
// This might actually be our end marker.
|
|
||||||
topdoc := &heredocs[len(heredocs)-1]
|
|
||||||
if topdoc.StartOfLine {
|
|
||||||
maybeMarker := bytes.TrimSpace(data[ts:te])
|
|
||||||
if bytes.Equal(maybeMarker, topdoc.Marker) {
|
|
||||||
// We actually emit two tokens here: the end-of-heredoc
|
|
||||||
// marker first, and then separately the newline that
|
|
||||||
// follows it. This then avoids issues with the closing
|
|
||||||
// marker consuming a newline that would normally be used
|
|
||||||
// to mark the end of an attribute definition.
|
|
||||||
// We might have either a \n sequence or an \r\n sequence
|
|
||||||
// here, so we must handle both.
|
|
||||||
nls := te-1
|
|
||||||
nle := te
|
|
||||||
te--
|
|
||||||
if data[te-1] == '\r' {
|
|
||||||
// back up one more byte
|
|
||||||
nls--
|
|
||||||
te--
|
|
||||||
}
|
|
||||||
token(TokenCHeredoc);
|
|
||||||
ts = nls
|
|
||||||
te = nle
|
|
||||||
token(TokenNewline);
|
|
||||||
heredocs = heredocs[:len(heredocs)-1]
|
|
||||||
fret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
topdoc.StartOfLine = true;
|
|
||||||
token(TokenStringLit);
|
|
||||||
}
|
|
||||||
|
|
||||||
action heredocLiteralMidline {
|
|
||||||
// This action is called when a heredoc literal _doesn't_ end
|
|
||||||
// with a newline character, e.g. because we're about to enter
|
|
||||||
// an interpolation sequence.
|
|
||||||
heredocs[len(heredocs)-1].StartOfLine = false;
|
|
||||||
token(TokenStringLit);
|
|
||||||
}
|
|
||||||
|
|
||||||
action bareTemplateLiteral {
|
|
||||||
token(TokenStringLit);
|
|
||||||
}
|
|
||||||
|
|
||||||
action beginTemplateInterp {
|
|
||||||
token(TokenTemplateInterp);
|
|
||||||
braces++;
|
|
||||||
retBraces = append(retBraces, braces);
|
|
||||||
if len(heredocs) > 0 {
|
|
||||||
heredocs[len(heredocs)-1].StartOfLine = false;
|
|
||||||
}
|
|
||||||
fcall main;
|
|
||||||
}
|
|
||||||
|
|
||||||
action beginTemplateControl {
|
|
||||||
token(TokenTemplateControl);
|
|
||||||
braces++;
|
|
||||||
retBraces = append(retBraces, braces);
|
|
||||||
if len(heredocs) > 0 {
|
|
||||||
heredocs[len(heredocs)-1].StartOfLine = false;
|
|
||||||
}
|
|
||||||
fcall main;
|
|
||||||
}
|
|
||||||
|
|
||||||
action openBrace {
|
|
||||||
token(TokenOBrace);
|
|
||||||
braces++;
|
|
||||||
}
|
|
||||||
|
|
||||||
action closeBrace {
|
|
||||||
if len(retBraces) > 0 && retBraces[len(retBraces)-1] == braces {
|
|
||||||
token(TokenTemplateSeqEnd);
|
|
||||||
braces--;
|
|
||||||
retBraces = retBraces[0:len(retBraces)-1]
|
|
||||||
fret;
|
|
||||||
} else {
|
|
||||||
token(TokenCBrace);
|
|
||||||
braces--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
action closeTemplateSeqEatWhitespace {
|
|
||||||
// Only consume from the retBraces stack and return if we are at
|
|
||||||
// a suitable brace nesting level, otherwise things will get
|
|
||||||
// confused. (Not entering this branch indicates a syntax error,
|
|
||||||
// which we will catch in the parser.)
|
|
||||||
if len(retBraces) > 0 && retBraces[len(retBraces)-1] == braces {
|
|
||||||
token(TokenTemplateSeqEnd);
|
|
||||||
braces--;
|
|
||||||
retBraces = retBraces[0:len(retBraces)-1]
|
|
||||||
fret;
|
|
||||||
} else {
|
|
||||||
// We intentionally generate a TokenTemplateSeqEnd here,
|
|
||||||
// even though the user apparently wanted a brace, because
|
|
||||||
// we want to allow the parser to catch the incorrect use
|
|
||||||
// of a ~} to balance a generic opening brace, rather than
|
|
||||||
// a template sequence.
|
|
||||||
token(TokenTemplateSeqEnd);
|
|
||||||
braces--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TemplateInterp = "${" ("~")?;
|
|
||||||
TemplateControl = "%{" ("~")?;
|
|
||||||
EndStringTmpl = '"';
|
|
||||||
NewlineChars = ("\r"|"\n");
|
|
||||||
NewlineCharsSeq = NewlineChars+;
|
|
||||||
StringLiteralChars = (AnyUTF8 - NewlineChars);
|
|
||||||
TemplateIgnoredNonBrace = (^'{' %{ fhold; });
|
|
||||||
TemplateNotInterp = '$' (TemplateIgnoredNonBrace | TemplateInterp);
|
|
||||||
TemplateNotControl = '%' (TemplateIgnoredNonBrace | TemplateControl);
|
|
||||||
QuotedStringLiteralWithEsc = ('\\' StringLiteralChars) | (StringLiteralChars - ("$" | '%' | '"' | "\\"));
|
|
||||||
TemplateStringLiteral = (
|
|
||||||
(TemplateNotInterp) |
|
|
||||||
(TemplateNotControl) |
|
|
||||||
(QuotedStringLiteralWithEsc)+
|
|
||||||
);
|
|
||||||
HeredocStringLiteral = (
|
|
||||||
(TemplateNotInterp) |
|
|
||||||
(TemplateNotControl) |
|
|
||||||
(StringLiteralChars - ("$" | '%'))*
|
|
||||||
);
|
|
||||||
BareStringLiteral = (
|
|
||||||
(TemplateNotInterp) |
|
|
||||||
(TemplateNotControl) |
|
|
||||||
(StringLiteralChars - ("$" | '%'))*
|
|
||||||
) Newline?;
|
|
||||||
|
|
||||||
stringTemplate := |*
|
|
||||||
TemplateInterp => beginTemplateInterp;
|
|
||||||
TemplateControl => beginTemplateControl;
|
|
||||||
EndStringTmpl => endStringTemplate;
|
|
||||||
TemplateStringLiteral => { token(TokenQuotedLit); };
|
|
||||||
NewlineCharsSeq => { token(TokenQuotedNewline); };
|
|
||||||
AnyUTF8 => { token(TokenInvalid); };
|
|
||||||
BrokenUTF8 => { token(TokenBadUTF8); };
|
|
||||||
*|;
|
|
||||||
|
|
||||||
heredocTemplate := |*
|
|
||||||
TemplateInterp => beginTemplateInterp;
|
|
||||||
TemplateControl => beginTemplateControl;
|
|
||||||
HeredocStringLiteral EndOfLine => heredocLiteralEOL;
|
|
||||||
HeredocStringLiteral => heredocLiteralMidline;
|
|
||||||
BrokenUTF8 => { token(TokenBadUTF8); };
|
|
||||||
*|;
|
|
||||||
|
|
||||||
bareTemplate := |*
|
|
||||||
TemplateInterp => beginTemplateInterp;
|
|
||||||
TemplateControl => beginTemplateControl;
|
|
||||||
BareStringLiteral => bareTemplateLiteral;
|
|
||||||
BrokenUTF8 => { token(TokenBadUTF8); };
|
|
||||||
*|;
|
|
||||||
|
|
||||||
identOnly := |*
|
|
||||||
Ident => { token(TokenIdent) };
|
|
||||||
BrokenUTF8 => { token(TokenBadUTF8) };
|
|
||||||
AnyUTF8 => { token(TokenInvalid) };
|
|
||||||
*|;
|
|
||||||
|
|
||||||
main := |*
|
|
||||||
Spaces => {};
|
|
||||||
NumberLit => { token(TokenNumberLit) };
|
|
||||||
Ident => { token(TokenIdent) };
|
|
||||||
|
|
||||||
Comment => { token(TokenComment) };
|
|
||||||
Newline => { token(TokenNewline) };
|
|
||||||
|
|
||||||
EqualOp => { token(TokenEqualOp); };
|
|
||||||
NotEqual => { token(TokenNotEqual); };
|
|
||||||
GreaterThanEqual => { token(TokenGreaterThanEq); };
|
|
||||||
LessThanEqual => { token(TokenLessThanEq); };
|
|
||||||
LogicalAnd => { token(TokenAnd); };
|
|
||||||
LogicalOr => { token(TokenOr); };
|
|
||||||
Ellipsis => { token(TokenEllipsis); };
|
|
||||||
FatArrow => { token(TokenFatArrow); };
|
|
||||||
SelfToken => { selfToken() };
|
|
||||||
|
|
||||||
"{" => openBrace;
|
|
||||||
"}" => closeBrace;
|
|
||||||
|
|
||||||
"~}" => closeTemplateSeqEatWhitespace;
|
|
||||||
|
|
||||||
BeginStringTmpl => beginStringTemplate;
|
|
||||||
BeginHeredocTmpl => beginHeredocTemplate;
|
|
||||||
|
|
||||||
BrokenUTF8 => { token(TokenBadUTF8) };
|
|
||||||
AnyUTF8 => { token(TokenInvalid) };
|
|
||||||
*|;
|
|
||||||
|
|
||||||
}%%
|
|
||||||
|
|
||||||
// Ragel state
|
|
||||||
p := 0 // "Pointer" into data
|
|
||||||
pe := len(data) // End-of-data "pointer"
|
|
||||||
ts := 0
|
|
||||||
te := 0
|
|
||||||
act := 0
|
|
||||||
eof := pe
|
|
||||||
var stack []int
|
|
||||||
var top int
|
|
||||||
|
|
||||||
var cs int // current state
|
|
||||||
switch mode {
|
|
||||||
case scanNormal:
|
|
||||||
cs = hcltok_en_main
|
|
||||||
case scanTemplate:
|
|
||||||
cs = hcltok_en_bareTemplate
|
|
||||||
case scanIdentOnly:
|
|
||||||
cs = hcltok_en_identOnly
|
|
||||||
default:
|
|
||||||
panic("invalid scanMode")
|
|
||||||
}
|
|
||||||
|
|
||||||
braces := 0
|
|
||||||
var retBraces []int // stack of brace levels that cause us to use fret
|
|
||||||
var heredocs []heredocInProgress // stack of heredocs we're currently processing
|
|
||||||
|
|
||||||
%%{
|
|
||||||
prepush {
|
|
||||||
stack = append(stack, 0);
|
|
||||||
}
|
|
||||||
postpop {
|
|
||||||
stack = stack[:len(stack)-1];
|
|
||||||
}
|
|
||||||
}%%
|
|
||||||
|
|
||||||
// Make Go compiler happy
|
|
||||||
_ = ts
|
|
||||||
_ = te
|
|
||||||
_ = act
|
|
||||||
_ = eof
|
|
||||||
|
|
||||||
token := func (ty TokenType) {
|
|
||||||
f.emitToken(ty, ts, te)
|
|
||||||
}
|
|
||||||
selfToken := func () {
|
|
||||||
b := data[ts:te]
|
|
||||||
if len(b) != 1 {
|
|
||||||
// should never happen
|
|
||||||
panic("selfToken only works for single-character tokens")
|
|
||||||
}
|
|
||||||
f.emitToken(TokenType(b[0]), ts, te)
|
|
||||||
}
|
|
||||||
|
|
||||||
%%{
|
|
||||||
write init nocs;
|
|
||||||
write exec;
|
|
||||||
}%%
|
|
||||||
|
|
||||||
// If we fall out here without being in a final state then we've
|
|
||||||
// encountered something that the scanner can't match, which we'll
|
|
||||||
// deal with as an invalid.
|
|
||||||
if cs < hcltok_first_final {
|
|
||||||
if mode == scanTemplate && len(stack) == 0 {
|
|
||||||
// If we're scanning a bare template then any straggling
|
|
||||||
// top-level stuff is actually literal string, rather than
|
|
||||||
// invalid. This handles the case where the template ends
|
|
||||||
// with a single "$" or "%", which trips us up because we
|
|
||||||
// want to see another character to decide if it's a sequence
|
|
||||||
// or an escape.
|
|
||||||
f.emitToken(TokenStringLit, ts, len(data))
|
|
||||||
} else {
|
|
||||||
f.emitToken(TokenInvalid, ts, len(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We always emit a synthetic EOF token at the end, since it gives the
|
|
||||||
// parser position information for an "unexpected EOF" diagnostic.
|
|
||||||
f.emitToken(TokenEOF, len(data), len(data))
|
|
||||||
|
|
||||||
return f.Tokens
|
|
||||||
}
|
|
|
@ -1,926 +0,0 @@
|
||||||
# HCL Native Syntax Specification
|
|
||||||
|
|
||||||
This is the specification of the syntax and semantics of the native syntax
|
|
||||||
for HCL. HCL is a system for defining configuration languages for applications.
|
|
||||||
The HCL information model is designed to support multiple concrete syntaxes
|
|
||||||
for configuration, but this native syntax is considered the primary format
|
|
||||||
and is optimized for human authoring and maintenance, as opposed to machine
|
|
||||||
generation of configuration.
|
|
||||||
|
|
||||||
The language consists of three integrated sub-languages:
|
|
||||||
|
|
||||||
- The _structural_ language defines the overall hierarchical configuration
|
|
||||||
structure, and is a serialization of HCL bodies, blocks and attributes.
|
|
||||||
|
|
||||||
- The _expression_ language is used to express attribute values, either as
|
|
||||||
literals or as derivations of other values.
|
|
||||||
|
|
||||||
- The _template_ language is used to compose values together into strings,
|
|
||||||
as one of several types of expression in the expression language.
|
|
||||||
|
|
||||||
In normal use these three sub-languages are used together within configuration
|
|
||||||
files to describe an overall configuration, with the structural language
|
|
||||||
being used at the top level. The expression and template languages can also
|
|
||||||
be used in isolation, to implement features such as REPLs, debuggers, and
|
|
||||||
integration into more limited HCL syntaxes such as the JSON profile.
|
|
||||||
|
|
||||||
## Syntax Notation
|
|
||||||
|
|
||||||
Within this specification a semi-formal notation is used to illustrate the
|
|
||||||
details of syntax. This notation is intended for human consumption rather
|
|
||||||
than machine consumption, with the following conventions:
|
|
||||||
|
|
||||||
- A naked name starting with an uppercase letter is a global production,
|
|
||||||
common to all of the syntax specifications in this document.
|
|
||||||
- A naked name starting with a lowercase letter is a local production,
|
|
||||||
meaningful only within the specification where it is defined.
|
|
||||||
- Double and single quotes (`"` and `'`) are used to mark literal character
|
|
||||||
sequences, which may be either punctuation markers or keywords.
|
|
||||||
- The default operator for combining items, which has no punctuation,
|
|
||||||
is concatenation.
|
|
||||||
- The symbol `|` indicates that any one of its left and right operands may
|
|
||||||
be present.
|
|
||||||
- The `*` symbol indicates zero or more repetitions of the item to its left.
|
|
||||||
- The `?` symbol indicates zero or one of the item to its left.
|
|
||||||
- Parentheses (`(` and `)`) are used to group items together to apply
|
|
||||||
the `|`, `*` and `?` operators to them collectively.
|
|
||||||
|
|
||||||
The grammar notation does not fully describe the language. The prose may
|
|
||||||
augment or conflict with the illustrated grammar. In case of conflict, prose
|
|
||||||
has priority.
|
|
||||||
|
|
||||||
## Source Code Representation
|
|
||||||
|
|
||||||
Source code is unicode text expressed in the UTF-8 encoding. The language
|
|
||||||
itself does not perform unicode normalization, so syntax features such as
|
|
||||||
identifiers are sequences of unicode code points and so e.g. a precombined
|
|
||||||
accented character is distinct from a letter associated with a combining
|
|
||||||
accent. (String literals have some special handling with regard to Unicode
|
|
||||||
normalization which will be covered later in the relevant section.)
|
|
||||||
|
|
||||||
UTF-8 encoded Unicode byte order marks are not permitted. Invalid or
|
|
||||||
non-normalized UTF-8 encoding is always a parse error.
|
|
||||||
|
|
||||||
## Lexical Elements
|
|
||||||
|
|
||||||
### Comments and Whitespace
|
|
||||||
|
|
||||||
Comments and Whitespace are recognized as lexical elements but are ignored
|
|
||||||
except as described below.
|
|
||||||
|
|
||||||
Whitespace is defined as a sequence of zero or more space characters
|
|
||||||
(U+0020). Newline sequences (either U+000A or U+000D followed by U+000A)
|
|
||||||
are _not_ considered whitespace but are ignored as such in certain contexts.
|
|
||||||
|
|
||||||
Horizontal tab characters (U+0009) are not considered to be whitespace and
|
|
||||||
are not valid within HCL native syntax.
|
|
||||||
|
|
||||||
Comments serve as program documentation and come in two forms:
|
|
||||||
|
|
||||||
- _Line comments_ start with either the `//` or `#` sequences and end with
|
|
||||||
the next newline sequence. A line comments is considered equivalent to a
|
|
||||||
newline sequence.
|
|
||||||
|
|
||||||
- _Inline comments_ start with the `/*` sequence and end with the `*/`
|
|
||||||
sequence, and may have any characters within except the ending sequence.
|
|
||||||
An inline comments is considered equivalent to a whitespace sequence.
|
|
||||||
|
|
||||||
Comments and whitespace cannot begin within within other comments, or within
|
|
||||||
template literals except inside an interpolation sequence or template directive.
|
|
||||||
|
|
||||||
### Identifiers
|
|
||||||
|
|
||||||
Identifiers name entities such as blocks, attributes and expression variables.
|
|
||||||
Identifiers are interpreted as per [UAX #31][uax31] Section 2. Specifically,
|
|
||||||
their syntax is defined in terms of the `ID_Start` and `ID_Continue`
|
|
||||||
character properties as follows:
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
Identifier = ID_Start (ID_Continue | '-')*;
|
|
||||||
```
|
|
||||||
|
|
||||||
The Unicode specification provides the normative requirements for identifier
|
|
||||||
parsing. Non-normatively, the spirit of this specification is that `ID_Start`
|
|
||||||
consists of Unicode letter and certain unambiguous punctuation tokens, while
|
|
||||||
`ID_Continue` augments that set with Unicode digits, combining marks, etc.
|
|
||||||
|
|
||||||
The dash character `-` is additionally allowed in identifiers, even though
|
|
||||||
that is not part of the unicode `ID_Continue` definition. This is to allow
|
|
||||||
attribute names and block type names to contain dashes, although underscores
|
|
||||||
as word separators are considered the idiomatic usage.
|
|
||||||
|
|
||||||
[uax31]: http://unicode.org/reports/tr31/ "Unicode Identifier and Pattern Syntax"
|
|
||||||
|
|
||||||
### Keywords
|
|
||||||
|
|
||||||
There are no globally-reserved words, but in some contexts certain identifiers
|
|
||||||
are reserved to function as keywords. These are discussed further in the
|
|
||||||
relevant documentation sections that follow. In such situations, the
|
|
||||||
identifier's role as a keyword supersedes any other valid interpretation that
|
|
||||||
may be possible. Outside of these specific situations, the keywords have no
|
|
||||||
special meaning and are interpreted as regular identifiers.
|
|
||||||
|
|
||||||
### Operators and Delimiters
|
|
||||||
|
|
||||||
The following character sequences represent operators, delimiters, and other
|
|
||||||
special tokens:
|
|
||||||
|
|
||||||
```
|
|
||||||
+ && == < : { [ ( ${
|
|
||||||
- || != > ? } ] ) %{
|
|
||||||
* ! <= = .
|
|
||||||
/ >= => ,
|
|
||||||
% ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Numeric Literals
|
|
||||||
|
|
||||||
A numeric literal is a decimal representation of a
|
|
||||||
real number. It has an integer part, a fractional part,
|
|
||||||
and an exponent part.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
NumericLit = decimal+ ("." decimal+)? (expmark decimal+)?;
|
|
||||||
decimal = '0' .. '9';
|
|
||||||
expmark = ('e' | 'E') ("+" | "-")?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Structural Elements
|
|
||||||
|
|
||||||
The structural language consists of syntax representing the following
|
|
||||||
constructs:
|
|
||||||
|
|
||||||
- _Attributes_, which assign a value to a specified name.
|
|
||||||
- _Blocks_, which create a child body annotated by a type and optional labels.
|
|
||||||
- _Body Content_, which consists of a collection of attributes and blocks.
|
|
||||||
|
|
||||||
These constructs correspond to the similarly-named concepts in the
|
|
||||||
language-agnostic HCL information model.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
ConfigFile = Body;
|
|
||||||
Body = (Attribute | Block | OneLineBlock)*;
|
|
||||||
Attribute = Identifier "=" Expression Newline;
|
|
||||||
Block = Identifier (StringLit|Identifier)* "{" Newline Body "}" Newline;
|
|
||||||
OneLineBlock = Identifier (StringLit|Identifier)* "{" (Identifier "=" Expression)? "}" Newline;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration Files
|
|
||||||
|
|
||||||
A _configuration file_ is a sequence of characters whose top-level is
|
|
||||||
interpreted as a Body.
|
|
||||||
|
|
||||||
### Bodies
|
|
||||||
|
|
||||||
A _body_ is a collection of associated attributes and blocks. The meaning of
|
|
||||||
this association is defined by the calling application.
|
|
||||||
|
|
||||||
### Attribute Definitions
|
|
||||||
|
|
||||||
An _attribute definition_ assigns a value to a particular attribute name within
|
|
||||||
a body. Each distinct attribute name may be defined no more than once within a
|
|
||||||
single body.
|
|
||||||
|
|
||||||
The attribute value is given as an expression, which is retained literally
|
|
||||||
for later evaluation by the calling application.
|
|
||||||
|
|
||||||
### Blocks
|
|
||||||
|
|
||||||
A _block_ creates a child body that is annotated with a block _type_ and
|
|
||||||
zero or more block _labels_. Blocks create a structural hierarchy which can be
|
|
||||||
interpreted by the calling application.
|
|
||||||
|
|
||||||
Block labels can either be quoted literal strings or naked identifiers.
|
|
||||||
|
|
||||||
## Expressions
|
|
||||||
|
|
||||||
The expression sub-language is used within attribute definitions to specify
|
|
||||||
values.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
Expression = (
|
|
||||||
ExprTerm |
|
|
||||||
Operation |
|
|
||||||
Conditional
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Types
|
|
||||||
|
|
||||||
The value types used within the expression language are those defined by the
|
|
||||||
syntax-agnostic HCL information model. An expression may return any valid
|
|
||||||
type, but only a subset of the available types have first-class syntax.
|
|
||||||
A calling application may make other types available via _variables_ and
|
|
||||||
_functions_.
|
|
||||||
|
|
||||||
### Expression Terms
|
|
||||||
|
|
||||||
Expression _terms_ are the operands for unary and binary expressions, as well
|
|
||||||
as acting as expressions in their own right.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
ExprTerm = (
|
|
||||||
LiteralValue |
|
|
||||||
CollectionValue |
|
|
||||||
TemplateExpr |
|
|
||||||
VariableExpr |
|
|
||||||
FunctionCall |
|
|
||||||
ForExpr |
|
|
||||||
ExprTerm Index |
|
|
||||||
ExprTerm GetAttr |
|
|
||||||
ExprTerm Splat |
|
|
||||||
"(" Expression ")"
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
The productions for these different term types are given in their corresponding
|
|
||||||
sections.
|
|
||||||
|
|
||||||
Between the `(` and `)` characters denoting a sub-expression, newline
|
|
||||||
characters are ignored as whitespace.
|
|
||||||
|
|
||||||
### Literal Values
|
|
||||||
|
|
||||||
A _literal value_ immediately represents a particular value of a primitive
|
|
||||||
type.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
LiteralValue = (
|
|
||||||
NumericLit |
|
|
||||||
"true" |
|
|
||||||
"false" |
|
|
||||||
"null"
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
- Numeric literals represent values of type _number_.
|
|
||||||
- The `true` and `false` keywords represent values of type _bool_.
|
|
||||||
- The `null` keyword represents a null value of the dynamic pseudo-type.
|
|
||||||
|
|
||||||
String literals are not directly available in the expression sub-language, but
|
|
||||||
are available via the template sub-language, which can in turn be incorporated
|
|
||||||
via _template expressions_.
|
|
||||||
|
|
||||||
### Collection Values
|
|
||||||
|
|
||||||
A _collection value_ combines zero or more other expressions to produce a
|
|
||||||
collection value.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
CollectionValue = tuple | object;
|
|
||||||
tuple = "[" (
|
|
||||||
(Expression ("," Expression)* ","?)?
|
|
||||||
) "]";
|
|
||||||
object = "{" (
|
|
||||||
(objectelem ("," objectelem)* ","?)?
|
|
||||||
) "}";
|
|
||||||
objectelem = (Identifier | Expression) "=" Expression;
|
|
||||||
```
|
|
||||||
|
|
||||||
Only tuple and object values can be directly constructed via native syntax.
|
|
||||||
Tuple and object values can in turn be converted to list, set and map values
|
|
||||||
with other operations, which behaves as defined by the syntax-agnostic HCL
|
|
||||||
information model.
|
|
||||||
|
|
||||||
When specifying an object element, an identifier is interpreted as a literal
|
|
||||||
attribute name as opposed to a variable reference. To populate an item key
|
|
||||||
from a variable, use parentheses to disambiguate:
|
|
||||||
|
|
||||||
- `{foo = "baz"}` is interpreted as an attribute literally named `foo`.
|
|
||||||
- `{(foo) = "baz"}` is interpreted as an attribute whose name is taken
|
|
||||||
from the variable named `foo`.
|
|
||||||
|
|
||||||
Between the open and closing delimiters of these sequences, newline sequences
|
|
||||||
are ignored as whitespace.
|
|
||||||
|
|
||||||
There is a syntax ambiguity between _for expressions_ and collection values
|
|
||||||
whose first element is a reference to a variable named `for`. The
|
|
||||||
_for expression_ interpretation has priority, so to produce a tuple whose
|
|
||||||
first element is the value of a variable named `for`, or an object with a
|
|
||||||
key named `for`, use parentheses to disambiguate:
|
|
||||||
|
|
||||||
- `[for, foo, baz]` is a syntax error.
|
|
||||||
- `[(for), foo, baz]` is a tuple whose first element is the value of variable
|
|
||||||
`for`.
|
|
||||||
- `{for: 1, baz: 2}` is a syntax error.
|
|
||||||
- `{(for): 1, baz: 2}` is an object with an attribute literally named `for`.
|
|
||||||
- `{baz: 2, for: 1}` is equivalent to the previous example, and resolves the
|
|
||||||
ambiguity by reordering.
|
|
||||||
|
|
||||||
### Template Expressions
|
|
||||||
|
|
||||||
A _template expression_ embeds a program written in the template sub-language
|
|
||||||
as an expression. Template expressions come in two forms:
|
|
||||||
|
|
||||||
- A _quoted_ template expression is delimited by quote characters (`"`) and
|
|
||||||
defines a template as a single-line expression with escape characters.
|
|
||||||
- A _heredoc_ template expression is introduced by a `<<` sequence and
|
|
||||||
defines a template via a multi-line sequence terminated by a user-chosen
|
|
||||||
delimiter.
|
|
||||||
|
|
||||||
In both cases the template interpolation and directive syntax is available for
|
|
||||||
use within the delimiters, and any text outside of these special sequences is
|
|
||||||
interpreted as a literal string.
|
|
||||||
|
|
||||||
In _quoted_ template expressions any literal string sequences within the
|
|
||||||
template behave in a special way: literal newline sequences are not permitted
|
|
||||||
and instead _escape sequences_ can be included, starting with the
|
|
||||||
backslash `\`:
|
|
||||||
|
|
||||||
```
|
|
||||||
\n Unicode newline control character
|
|
||||||
\r Unicode carriage return control character
|
|
||||||
\t Unicode tab control character
|
|
||||||
\" Literal quote mark, used to prevent interpretation as end of string
|
|
||||||
\\ Literal backslash, used to prevent interpretation as escape sequence
|
|
||||||
\uNNNN Unicode character from Basic Multilingual Plane (NNNN is four hexadecimal digits)
|
|
||||||
\UNNNNNNNN Unicode character from supplementary planes (NNNNNNNN is eight hexadecimal digits)
|
|
||||||
```
|
|
||||||
|
|
||||||
The _heredoc_ template expression type is introduced by either `<<` or `<<-`,
|
|
||||||
followed by an identifier. The template expression ends when the given
|
|
||||||
identifier subsequently appears again on a line of its own.
|
|
||||||
|
|
||||||
If a heredoc template is introduced with the `<<-` symbol, any literal string
|
|
||||||
at the start of each line is analyzed to find the minimum number of leading
|
|
||||||
spaces, and then that number of prefix spaces is removed from all line-leading
|
|
||||||
literal strings. The final closing marker may also have an arbitrary number
|
|
||||||
of spaces preceding it on its line.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
TemplateExpr = quotedTemplate | heredocTemplate;
|
|
||||||
quotedTemplate = (as defined in prose above);
|
|
||||||
heredocTemplate = (
|
|
||||||
("<<" | "<<-") Identifier Newline
|
|
||||||
(content as defined in prose above)
|
|
||||||
Identifier Newline
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
A quoted template expression containing only a single literal string serves
|
|
||||||
as a syntax for defining literal string _expressions_. In certain contexts
|
|
||||||
the template syntax is restricted in this manner:
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
StringLit = '"' (quoted literals as defined in prose above) '"';
|
|
||||||
```
|
|
||||||
|
|
||||||
The `StringLit` production permits the escape sequences discussed for quoted
|
|
||||||
template expressions as above, but does _not_ permit template interpolation
|
|
||||||
or directive sequences.
|
|
||||||
|
|
||||||
### Variables and Variable Expressions
|
|
||||||
|
|
||||||
A _variable_ is a value that has been assigned a symbolic name. Variables are
|
|
||||||
made available for use in expressions by the calling application, by populating
|
|
||||||
the _global scope_ used for expression evaluation.
|
|
||||||
|
|
||||||
Variables can also be created by expressions themselves, which always creates
|
|
||||||
a _child scope_ that incorporates the variables from its parent scope but
|
|
||||||
(re-)defines zero or more names with new values.
|
|
||||||
|
|
||||||
The value of a variable is accessed using a _variable expression_, which is
|
|
||||||
a standalone `Identifier` whose name corresponds to a defined variable:
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
VariableExpr = Identifier;
|
|
||||||
```
|
|
||||||
|
|
||||||
Variables in a particular scope are immutable, but child scopes may _hide_
|
|
||||||
a variable from an ancestor scope by defining a new variable of the same name.
|
|
||||||
When looking up variables, the most locally-defined variable of the given name
|
|
||||||
is used, and ancestor-scoped variables of the same name cannot be accessed.
|
|
||||||
|
|
||||||
No direct syntax is provided for declaring or assigning variables, but other
|
|
||||||
expression constructs implicitly create child scopes and define variables as
|
|
||||||
part of their evaluation.
|
|
||||||
|
|
||||||
### Functions and Function Calls
|
|
||||||
|
|
||||||
A _function_ is an operation that has been assigned a symbolic name. Functions
|
|
||||||
are made available for use in expressions by the calling application, by
|
|
||||||
populating the _function table_ used for expression evaluation.
|
|
||||||
|
|
||||||
The namespace of functions is distinct from the namespace of variables. A
|
|
||||||
function and a variable may share the same name with no implication that they
|
|
||||||
are in any way related.
|
|
||||||
|
|
||||||
A function can be executed via a _function call_ expression:
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
FunctionCall = Identifier "(" arguments ")";
|
|
||||||
Arguments = (
|
|
||||||
() ||
|
|
||||||
(Expression ("," Expression)* ("," | "...")?)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
The definition of functions and the semantics of calling them are defined by
|
|
||||||
the language-agnostic HCL information model. The given arguments are mapped
|
|
||||||
onto the function's _parameters_ and the result of a function call expression
|
|
||||||
is the return value of the named function when given those arguments.
|
|
||||||
|
|
||||||
If the final argument expression is followed by the ellipsis symbol (`...`),
|
|
||||||
the final argument expression must evaluate to either a list or tuple value.
|
|
||||||
The elements of the value are each mapped to a single parameter of the
|
|
||||||
named function, beginning at the first parameter remaining after all other
|
|
||||||
argument expressions have been mapped.
|
|
||||||
|
|
||||||
Within the parentheses that delimit the function arguments, newline sequences
|
|
||||||
are ignored as whitespace.
|
|
||||||
|
|
||||||
### For Expressions
|
|
||||||
|
|
||||||
A _for expression_ is a construct for constructing a collection by projecting
|
|
||||||
the items from another collection.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
ForExpr = forTupleExpr | forObjectExpr;
|
|
||||||
forTupleExpr = "[" forIntro Expression forCond? "]";
|
|
||||||
forObjectExpr = "{" forIntro Expression "=>" Expression "..."? forCond? "}";
|
|
||||||
forIntro = "for" Identifier ("," Identifier)? "in" Expression ":";
|
|
||||||
forCond = "if" Expression;
|
|
||||||
```
|
|
||||||
|
|
||||||
The punctuation used to delimit a for expression decide whether it will produce
|
|
||||||
a tuple value (`[` and `]`) or an object value (`{` and `}`).
|
|
||||||
|
|
||||||
The "introduction" is equivalent in both cases: the keyword `for` followed by
|
|
||||||
either one or two identifiers separated by a comma which define the temporary
|
|
||||||
variable names used for iteration, followed by the keyword `in` and then
|
|
||||||
an expression that must evaluate to a value that can be iterated. The
|
|
||||||
introduction is then terminated by the colon (`:`) symbol.
|
|
||||||
|
|
||||||
If only one identifier is provided, it is the name of a variable that will
|
|
||||||
be temporarily assigned the value of each element during iteration. If both
|
|
||||||
are provided, the first is the key and the second is the value.
|
|
||||||
|
|
||||||
Tuple, object, list, map, and set types are iterable. The type of collection
|
|
||||||
used defines how the key and value variables are populated:
|
|
||||||
|
|
||||||
- For tuple and list types, the _key_ is the zero-based index into the
|
|
||||||
sequence for each element, and the _value_ is the element value. The
|
|
||||||
elements are visited in index order.
|
|
||||||
- For object and map types, the _key_ is the string attribute name or element
|
|
||||||
key, and the _value_ is the attribute or element value. The elements are
|
|
||||||
visited in the order defined by a lexicographic sort of the attribute names
|
|
||||||
or keys.
|
|
||||||
- For set types, the _key_ and _value_ are both the element value. The elements
|
|
||||||
are visited in an undefined but consistent order.
|
|
||||||
|
|
||||||
The expression after the colon and (in the case of object `for`) the expression
|
|
||||||
after the `=>` are both evaluated once for each element of the source
|
|
||||||
collection, in a local scope that defines the key and value variable names
|
|
||||||
specified.
|
|
||||||
|
|
||||||
The results of evaluating these expressions for each input element are used
|
|
||||||
to populate an element in the new collection. In the case of tuple `for`, the
|
|
||||||
single expression becomes an element, appending values to the tuple in visit
|
|
||||||
order. In the case of object `for`, the pair of expressions is used as an
|
|
||||||
attribute name and value respectively, creating an element in the resulting
|
|
||||||
object.
|
|
||||||
|
|
||||||
In the case of object `for`, it is an error if two input elements produce
|
|
||||||
the same result from the attribute name expression, since duplicate
|
|
||||||
attributes are not possible. If the ellipsis symbol (`...`) appears
|
|
||||||
immediately after the value expression, this activates the grouping mode in
|
|
||||||
which each value in the resulting object is a _tuple_ of all of the values
|
|
||||||
that were produced against each distinct key.
|
|
||||||
|
|
||||||
- `[for v in ["a", "b"]: v]` returns `["a", "b"]`.
|
|
||||||
- `[for i, v in ["a", "b"]: i]` returns `[0, 1]`.
|
|
||||||
- `{for i, v in ["a", "b"]: v => i}` returns `{a = 0, b = 1}`.
|
|
||||||
- `{for i, v in ["a", "a", "b"]: k => v}` produces an error, because attribute
|
|
||||||
`a` is defined twice.
|
|
||||||
- `{for i, v in ["a", "a", "b"]: v => i...}` returns `{a = [0, 1], b = [2]}`.
|
|
||||||
|
|
||||||
If the `if` keyword is used after the element expression(s), it applies an
|
|
||||||
additional predicate that can be used to conditionally filter elements from
|
|
||||||
the source collection from consideration. The expression following `if` is
|
|
||||||
evaluated once for each source element, in the same scope used for the
|
|
||||||
element expression(s). It must evaluate to a boolean value; if `true`, the
|
|
||||||
element will be evaluated as normal, while if `false` the element will be
|
|
||||||
skipped.
|
|
||||||
|
|
||||||
- `[for i, v in ["a", "b", "c"]: v if i < 2]` returns `["a", "b"]`.
|
|
||||||
|
|
||||||
If the collection value, element expression(s) or condition expression return
|
|
||||||
unknown values that are otherwise type-valid, the result is a value of the
|
|
||||||
dynamic pseudo-type.
|
|
||||||
|
|
||||||
### Index Operator
|
|
||||||
|
|
||||||
The _index_ operator returns the value of a single element of a collection
|
|
||||||
value. It is a postfix operator and can be applied to any value that has
|
|
||||||
a tuple, object, map, or list type.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
Index = "[" Expression "]";
|
|
||||||
```
|
|
||||||
|
|
||||||
The expression delimited by the brackets is the _key_ by which an element
|
|
||||||
will be looked up.
|
|
||||||
|
|
||||||
If the index operator is applied to a value of tuple or list type, the
|
|
||||||
key expression must be an non-negative integer number representing the
|
|
||||||
zero-based element index to access. If applied to a value of object or map
|
|
||||||
type, the key expression must be a string representing the attribute name
|
|
||||||
or element key. If the given key value is not of the appropriate type, a
|
|
||||||
conversion is attempted using the conversion rules from the HCL
|
|
||||||
syntax-agnostic information model.
|
|
||||||
|
|
||||||
An error is produced if the given key expression does not correspond to
|
|
||||||
an element in the collection, either because it is of an unconvertable type,
|
|
||||||
because it is outside the range of elements for a tuple or list, or because
|
|
||||||
the given attribute or key does not exist.
|
|
||||||
|
|
||||||
If either the collection or the key are an unknown value of an
|
|
||||||
otherwise-suitable type, the return value is an unknown value whose type
|
|
||||||
matches what type would be returned given known values, or a value of the
|
|
||||||
dynamic pseudo-type if type information alone cannot determine a suitable
|
|
||||||
return type.
|
|
||||||
|
|
||||||
Within the brackets that delimit the index key, newline sequences are ignored
|
|
||||||
as whitespace.
|
|
||||||
|
|
||||||
### Attribute Access Operator
|
|
||||||
|
|
||||||
The _attribute access_ operator returns the value of a single attribute in
|
|
||||||
an object value. It is a postfix operator and can be applied to any value
|
|
||||||
that has an object type.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
GetAttr = "." Identifier;
|
|
||||||
```
|
|
||||||
|
|
||||||
The given identifier is interpreted as the name of the attribute to access.
|
|
||||||
An error is produced if the object to which the operator is applied does not
|
|
||||||
have an attribute with the given name.
|
|
||||||
|
|
||||||
If the object is an unknown value of a type that has the attribute named, the
|
|
||||||
result is an unknown value of the attribute's type.
|
|
||||||
|
|
||||||
### Splat Operators
|
|
||||||
|
|
||||||
The _splat operators_ allow convenient access to attributes or elements of
|
|
||||||
elements in a tuple, list, or set value.
|
|
||||||
|
|
||||||
There are two kinds of "splat" operator:
|
|
||||||
|
|
||||||
- The _attribute-only_ splat operator supports only attribute lookups into
|
|
||||||
the elements from a list, but supports an arbitrary number of them.
|
|
||||||
|
|
||||||
- The _full_ splat operator additionally supports indexing into the elements
|
|
||||||
from a list, and allows any combination of attribute access and index
|
|
||||||
operations.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
Splat = attrSplat | fullSplat;
|
|
||||||
attrSplat = "." "*" GetAttr*;
|
|
||||||
fullSplat = "[" "*" "]" (GetAttr | Index)*;
|
|
||||||
```
|
|
||||||
|
|
||||||
The splat operators can be thought of as shorthands for common operations that
|
|
||||||
could otherwise be performed using _for expressions_:
|
|
||||||
|
|
||||||
- `tuple.*.foo.bar[0]` is approximately equivalent to
|
|
||||||
`[for v in tuple: v.foo.bar][0]`.
|
|
||||||
- `tuple[*].foo.bar[0]` is approximately equivalent to
|
|
||||||
`[for v in tuple: v.foo.bar[0]]`
|
|
||||||
|
|
||||||
Note the difference in how the trailing index operator is interpreted in
|
|
||||||
each case. This different interpretation is the key difference between the
|
|
||||||
_attribute-only_ and _full_ splat operators.
|
|
||||||
|
|
||||||
Splat operators have one additional behavior compared to the equivalent
|
|
||||||
_for expressions_ shown above: if a splat operator is applied to a value that
|
|
||||||
is _not_ of tuple, list, or set type, the value is coerced automatically into
|
|
||||||
a single-value list of the value type:
|
|
||||||
|
|
||||||
- `any_object.*.id` is equivalent to `[any_object.id]`, assuming that `any_object`
|
|
||||||
is a single object.
|
|
||||||
- `any_number.*` is equivalent to `[any_number]`, assuming that `any_number`
|
|
||||||
is a single number.
|
|
||||||
|
|
||||||
If applied to a null value that is not tuple, list, or set, the result is always
|
|
||||||
an empty tuple, which allows conveniently converting a possibly-null scalar
|
|
||||||
value into a tuple of zero or one elements. It is illegal to apply a splat
|
|
||||||
operator to a null value of tuple, list, or set type.
|
|
||||||
|
|
||||||
### Operations
|
|
||||||
|
|
||||||
Operations apply a particular operator to either one or two expression terms.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
Operation = unaryOp | binaryOp;
|
|
||||||
unaryOp = ("-" | "!") ExprTerm;
|
|
||||||
binaryOp = ExprTerm binaryOperator ExprTerm;
|
|
||||||
binaryOperator = compareOperator | arithmeticOperator | logicOperator;
|
|
||||||
compareOperator = "==" | "!=" | "<" | ">" | "<=" | ">=";
|
|
||||||
arithmeticOperator = "+" | "-" | "*" | "/" | "%";
|
|
||||||
logicOperator = "&&" | "||" | "!";
|
|
||||||
```
|
|
||||||
|
|
||||||
The unary operators have the highest precedence.
|
|
||||||
|
|
||||||
The binary operators are grouped into the following precedence levels:
|
|
||||||
|
|
||||||
```
|
|
||||||
Level Operators
|
|
||||||
6 * / %
|
|
||||||
5 + -
|
|
||||||
4 > >= < <=
|
|
||||||
3 == !=
|
|
||||||
2 &&
|
|
||||||
1 ||
|
|
||||||
```
|
|
||||||
|
|
||||||
Higher values of "level" bind tighter. Operators within the same precedence
|
|
||||||
level have left-to-right associativity. For example, `x / y * z` is equivalent
|
|
||||||
to `(x / y) * z`.
|
|
||||||
|
|
||||||
### Comparison Operators
|
|
||||||
|
|
||||||
Comparison operators always produce boolean values, as a result of testing
|
|
||||||
the relationship between two values.
|
|
||||||
|
|
||||||
The two equality operators apply to values of any type:
|
|
||||||
|
|
||||||
```
|
|
||||||
a == b equal
|
|
||||||
a != b not equal
|
|
||||||
```
|
|
||||||
|
|
||||||
Two values are equal if the are of identical types and their values are
|
|
||||||
equal as defined in the HCL syntax-agnostic information model. The equality
|
|
||||||
operators are commutative and opposite, such that `(a == b) == !(a != b)`
|
|
||||||
and `(a == b) == (b == a)` for all values `a` and `b`.
|
|
||||||
|
|
||||||
The four numeric comparison operators apply only to numbers:
|
|
||||||
|
|
||||||
```
|
|
||||||
a < b less than
|
|
||||||
a <= b less than or equal to
|
|
||||||
a > b greater than
|
|
||||||
a >= b greater than or equal to
|
|
||||||
```
|
|
||||||
|
|
||||||
If either operand of a comparison operator is a correctly-typed unknown value
|
|
||||||
or a value of the dynamic pseudo-type, the result is an unknown boolean.
|
|
||||||
|
|
||||||
### Arithmetic Operators
|
|
||||||
|
|
||||||
Arithmetic operators apply only to number values and always produce number
|
|
||||||
values as results.
|
|
||||||
|
|
||||||
```
|
|
||||||
a + b sum (addition)
|
|
||||||
a - b difference (subtraction)
|
|
||||||
a * b product (multiplication)
|
|
||||||
a / b quotient (division)
|
|
||||||
a % b remainder (modulo)
|
|
||||||
-a negation
|
|
||||||
```
|
|
||||||
|
|
||||||
Arithmetic operations are considered to be performed in an arbitrary-precision
|
|
||||||
number space.
|
|
||||||
|
|
||||||
If either operand of an arithmetic operator is an unknown number or a value
|
|
||||||
of the dynamic pseudo-type, the result is an unknown number.
|
|
||||||
|
|
||||||
### Logic Operators
|
|
||||||
|
|
||||||
Logic operators apply only to boolean values and always produce boolean values
|
|
||||||
as results.
|
|
||||||
|
|
||||||
```
|
|
||||||
a && b logical AND
|
|
||||||
a || b logical OR
|
|
||||||
!a logical NOT
|
|
||||||
```
|
|
||||||
|
|
||||||
If either operand of a logic operator is an unknown bool value or a value
|
|
||||||
of the dynamic pseudo-type, the result is an unknown bool value.
|
|
||||||
|
|
||||||
### Conditional Operator
|
|
||||||
|
|
||||||
The conditional operator allows selecting from one of two expressions based on
|
|
||||||
the outcome of a boolean expression.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
Conditional = Expression "?" Expression ":" Expression;
|
|
||||||
```
|
|
||||||
|
|
||||||
The first expression is the _predicate_, which is evaluated and must produce
|
|
||||||
a boolean result. If the predicate value is `true`, the result of the second
|
|
||||||
expression is the result of the conditional. If the predicate value is
|
|
||||||
`false`, the result of the third expression is the result of the conditional.
|
|
||||||
|
|
||||||
The second and third expressions must be of the same type or must be able to
|
|
||||||
unify into a common type using the type unification rules defined in the
|
|
||||||
HCL syntax-agnostic information model. This unified type is the result type
|
|
||||||
of the conditional, with both expressions converted as necessary to the
|
|
||||||
unified type.
|
|
||||||
|
|
||||||
If the predicate is an unknown boolean value or a value of the dynamic
|
|
||||||
pseudo-type then the result is an unknown value of the unified type of the
|
|
||||||
other two expressions.
|
|
||||||
|
|
||||||
If either the second or third expressions produce errors when evaluated,
|
|
||||||
these errors are passed through only if the erroneous expression is selected.
|
|
||||||
This allows for expressions such as
|
|
||||||
`length(some_list) > 0 ? some_list[0] : default` (given some suitable `length`
|
|
||||||
function) without producing an error when the predicate is `false`.
|
|
||||||
|
|
||||||
## Templates
|
|
||||||
|
|
||||||
The template sub-language is used within template expressions to concisely
|
|
||||||
combine strings and other values to produce other strings. It can also be
|
|
||||||
used in isolation as a standalone template language.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
Template = (
|
|
||||||
TemplateLiteral |
|
|
||||||
TemplateInterpolation |
|
|
||||||
TemplateDirective
|
|
||||||
)*
|
|
||||||
TemplateDirective = TemplateIf | TemplateFor;
|
|
||||||
```
|
|
||||||
|
|
||||||
A template behaves like an expression that always returns a string value.
|
|
||||||
The different elements of the template are evaluated and combined into a
|
|
||||||
single string to return. If any of the elements produce an unknown string
|
|
||||||
or a value of the dynamic pseudo-type, the result is an unknown string.
|
|
||||||
|
|
||||||
An important use-case for standalone templates is to enable the use of
|
|
||||||
expressions in alternative HCL syntaxes where a native expression grammar is
|
|
||||||
not available. For example, the HCL JSON profile treats the values of JSON
|
|
||||||
strings as standalone templates when attributes are evaluated in expression
|
|
||||||
mode.
|
|
||||||
|
|
||||||
### Template Literals
|
|
||||||
|
|
||||||
A template literal is a literal sequence of characters to include in the
|
|
||||||
resulting string. When the template sub-language is used standalone, a
|
|
||||||
template literal can contain any unicode character, with the exception
|
|
||||||
of the sequences that introduce interpolations and directives, and for the
|
|
||||||
sequences that escape those introductions.
|
|
||||||
|
|
||||||
The interpolation and directive introductions are escaped by doubling their
|
|
||||||
leading characters. The `${` sequence is escaped as `$${` and the `%{`
|
|
||||||
sequence is escaped as `%%{`.
|
|
||||||
|
|
||||||
When the template sub-language is embedded in the expression language via
|
|
||||||
_template expressions_, additional constraints and transforms are applied to
|
|
||||||
template literals as described in the definition of template expressions.
|
|
||||||
|
|
||||||
The value of a template literal can be modified by _strip markers_ in any
|
|
||||||
interpolations or directives that are adjacent to it. A strip marker is
|
|
||||||
a tilde (`~`) placed immediately after the opening `{` or before the closing
|
|
||||||
`}` of a template sequence:
|
|
||||||
|
|
||||||
- `hello ${~ "world" }` produces `"helloworld"`.
|
|
||||||
- `%{ if true ~} hello %{~ endif }` produces `"hello"`.
|
|
||||||
|
|
||||||
When a strip marker is present, any spaces adjacent to it in the corresponding
|
|
||||||
string literal (if any) are removed before producing the final value. Space
|
|
||||||
characters are interpreted as per Unicode's definition.
|
|
||||||
|
|
||||||
Stripping is done at syntax level rather than value level. Values returned
|
|
||||||
by interpolations or directives are not subject to stripping:
|
|
||||||
|
|
||||||
- `${"hello" ~}${" world"}` produces `"hello world"`, and not `"helloworld"`,
|
|
||||||
because the space is not in a template literal directly adjacent to the
|
|
||||||
strip marker.
|
|
||||||
|
|
||||||
### Template Interpolations
|
|
||||||
|
|
||||||
An _interpolation sequence_ evaluates an expression (written in the
|
|
||||||
expression sub-language), converts the result to a string value, and
|
|
||||||
replaces itself with the resulting string.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
TemplateInterpolation = ("${" | "${~") Expression ("}" | "~}";
|
|
||||||
```
|
|
||||||
|
|
||||||
If the expression result cannot be converted to a string, an error is
|
|
||||||
produced.
|
|
||||||
|
|
||||||
### Template If Directive
|
|
||||||
|
|
||||||
The template `if` directive is the template equivalent of the
|
|
||||||
_conditional expression_, allowing selection of one of two sub-templates based
|
|
||||||
on the value of a predicate expression.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
TemplateIf = (
|
|
||||||
("%{" | "%{~") "if" Expression ("}" | "~}")
|
|
||||||
Template
|
|
||||||
(
|
|
||||||
("%{" | "%{~") "else" ("}" | "~}")
|
|
||||||
Template
|
|
||||||
)?
|
|
||||||
("%{" | "%{~") "endif" ("}" | "~}")
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
The evaluation of the `if` directive is equivalent to the conditional
|
|
||||||
expression, with the following exceptions:
|
|
||||||
|
|
||||||
- The two sub-templates always produce strings, and thus the result value is
|
|
||||||
also always a string.
|
|
||||||
- The `else` clause may be omitted, in which case the conditional's third
|
|
||||||
expression result is implied to be the empty string.
|
|
||||||
|
|
||||||
### Template For Directive
|
|
||||||
|
|
||||||
The template `for` directive is the template equivalent of the _for expression_,
|
|
||||||
producing zero or more copies of its sub-template based on the elements of
|
|
||||||
a collection.
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
TemplateFor = (
|
|
||||||
("%{" | "%{~") "for" Identifier ("," Identifier) "in" Expression ("}" | "~}")
|
|
||||||
Template
|
|
||||||
("%{" | "%{~") "endfor" ("}" | "~}")
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
The evaluation of the `for` directive is equivalent to the _for expression_
|
|
||||||
when producing a tuple, with the following exceptions:
|
|
||||||
|
|
||||||
- The sub-template always produces a string.
|
|
||||||
- There is no equivalent of the "if" clause on the for expression.
|
|
||||||
- The elements of the resulting tuple are all converted to strings and
|
|
||||||
concatenated to produce a flat string result.
|
|
||||||
|
|
||||||
### Template Interpolation Unwrapping
|
|
||||||
|
|
||||||
As a special case, a template that consists only of a single interpolation,
|
|
||||||
with no surrounding literals, directives or other interpolations, is
|
|
||||||
"unwrapped". In this case, the result of the interpolation expression is
|
|
||||||
returned verbatim, without conversion to string.
|
|
||||||
|
|
||||||
This special case exists primarily to enable the native template language
|
|
||||||
to be used inside strings in alternative HCL syntaxes that lack a first-class
|
|
||||||
template or expression syntax. Unwrapping allows arbitrary expressions to be
|
|
||||||
used to populate attributes when strings in such languages are interpreted
|
|
||||||
as templates.
|
|
||||||
|
|
||||||
- `${true}` produces the boolean value `true`
|
|
||||||
- `${"${true}"}` produces the boolean value `true`, because both the inner
|
|
||||||
and outer interpolations are subject to unwrapping.
|
|
||||||
- `hello ${true}` produces the string `"hello true"`
|
|
||||||
- `${""}${true}` produces the string `"true"` because there are two
|
|
||||||
interpolation sequences, even though one produces an empty result.
|
|
||||||
- `%{ for v in [true] }${v}%{ endif }` produces the string `true` because
|
|
||||||
the presence of the `for` directive circumvents the unwrapping even though
|
|
||||||
the final result is a single value.
|
|
||||||
|
|
||||||
In some contexts this unwrapping behavior may be circumvented by the calling
|
|
||||||
application, by converting the final template result to string. This is
|
|
||||||
necessary, for example, if a standalone template is being used to produce
|
|
||||||
the direct contents of a file, since the result in that case must always be a
|
|
||||||
string.
|
|
||||||
|
|
||||||
## Static Analysis
|
|
||||||
|
|
||||||
The HCL static analysis operations are implemented for some expression types
|
|
||||||
in the native syntax, as described in the following sections.
|
|
||||||
|
|
||||||
A goal for static analysis of the native syntax is for the interpretation to
|
|
||||||
be as consistent as possible with the dynamic evaluation interpretation of
|
|
||||||
the given expression, though some deviations are intentionally made in order
|
|
||||||
to maximize the potential for analysis.
|
|
||||||
|
|
||||||
### Static List
|
|
||||||
|
|
||||||
The tuple construction syntax can be interpreted as a static list. All of
|
|
||||||
the expression elements given are returned as the static list elements,
|
|
||||||
with no further interpretation.
|
|
||||||
|
|
||||||
### Static Map
|
|
||||||
|
|
||||||
The object construction syntax can be interpreted as a static map. All of the
|
|
||||||
key/value pairs given are returned as the static pairs, with no further
|
|
||||||
interpretation.
|
|
||||||
|
|
||||||
The usual requirement that an attribute name be interpretable as a string
|
|
||||||
does not apply to this static analysis, allowing callers to provide map-like
|
|
||||||
constructs with different key types by building on the map syntax.
|
|
||||||
|
|
||||||
### Static Call
|
|
||||||
|
|
||||||
The function call syntax can be interpreted as a static call. The called
|
|
||||||
function name is returned verbatim and the given argument expressions are
|
|
||||||
returned as the static arguments, with no further interpretation.
|
|
||||||
|
|
||||||
### Static Traversal
|
|
||||||
|
|
||||||
A variable expression and any attached attribute access operations and
|
|
||||||
constant index operations can be interpreted as a static traversal.
|
|
||||||
|
|
||||||
The keywords `true`, `false` and `null` can also be interpreted as
|
|
||||||
static traversals, behaving as if they were references to variables of those
|
|
||||||
names, to allow callers to redefine the meaning of those keywords in certain
|
|
||||||
contexts.
|
|
|
@ -1,394 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AsHCLBlock returns the block data expressed as a *hcl.Block.
|
|
||||||
func (b *Block) AsHCLBlock() *hcl.Block {
|
|
||||||
if b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lastHeaderRange := b.TypeRange
|
|
||||||
if len(b.LabelRanges) > 0 {
|
|
||||||
lastHeaderRange = b.LabelRanges[len(b.LabelRanges)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return &hcl.Block{
|
|
||||||
Type: b.Type,
|
|
||||||
Labels: b.Labels,
|
|
||||||
Body: b.Body,
|
|
||||||
|
|
||||||
DefRange: hcl.RangeBetween(b.TypeRange, lastHeaderRange),
|
|
||||||
TypeRange: b.TypeRange,
|
|
||||||
LabelRanges: b.LabelRanges,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Body is the implementation of hcl.Body for the HCL native syntax.
|
|
||||||
type Body struct {
|
|
||||||
Attributes Attributes
|
|
||||||
Blocks Blocks
|
|
||||||
|
|
||||||
// These are used with PartialContent to produce a "remaining items"
|
|
||||||
// body to return. They are nil on all bodies fresh out of the parser.
|
|
||||||
hiddenAttrs map[string]struct{}
|
|
||||||
hiddenBlocks map[string]struct{}
|
|
||||||
|
|
||||||
SrcRange hcl.Range
|
|
||||||
EndRange hcl.Range // Final token of the body, for reporting missing items
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that *Body implements hcl.Body
|
|
||||||
var assertBodyImplBody hcl.Body = &Body{}
|
|
||||||
|
|
||||||
func (b *Body) walkChildNodes(w internalWalkFunc) {
|
|
||||||
w(b.Attributes)
|
|
||||||
w(b.Blocks)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Body) Range() hcl.Range {
|
|
||||||
return b.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
|
|
||||||
content, remainHCL, diags := b.PartialContent(schema)
|
|
||||||
|
|
||||||
// No we'll see if anything actually remains, to produce errors about
|
|
||||||
// extraneous items.
|
|
||||||
remain := remainHCL.(*Body)
|
|
||||||
|
|
||||||
for name, attr := range b.Attributes {
|
|
||||||
if _, hidden := remain.hiddenAttrs[name]; !hidden {
|
|
||||||
var suggestions []string
|
|
||||||
for _, attrS := range schema.Attributes {
|
|
||||||
if _, defined := content.Attributes[attrS.Name]; defined {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
suggestions = append(suggestions, attrS.Name)
|
|
||||||
}
|
|
||||||
suggestion := nameSuggestion(name, suggestions)
|
|
||||||
if suggestion != "" {
|
|
||||||
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
|
||||||
} else {
|
|
||||||
// Is there a block of the same name?
|
|
||||||
for _, blockS := range schema.Blocks {
|
|
||||||
if blockS.Type == name {
|
|
||||||
suggestion = fmt.Sprintf(" Did you mean to define a block of type %q?", name)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unsupported argument",
|
|
||||||
Detail: fmt.Sprintf("An argument named %q is not expected here.%s", name, suggestion),
|
|
||||||
Subject: &attr.NameRange,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, block := range b.Blocks {
|
|
||||||
blockTy := block.Type
|
|
||||||
if _, hidden := remain.hiddenBlocks[blockTy]; !hidden {
|
|
||||||
var suggestions []string
|
|
||||||
for _, blockS := range schema.Blocks {
|
|
||||||
suggestions = append(suggestions, blockS.Type)
|
|
||||||
}
|
|
||||||
suggestion := nameSuggestion(blockTy, suggestions)
|
|
||||||
if suggestion != "" {
|
|
||||||
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
|
||||||
} else {
|
|
||||||
// Is there an attribute of the same name?
|
|
||||||
for _, attrS := range schema.Attributes {
|
|
||||||
if attrS.Name == blockTy {
|
|
||||||
suggestion = fmt.Sprintf(" Did you mean to define argument %q? If so, use the equals sign to assign it a value.", blockTy)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unsupported block type",
|
|
||||||
Detail: fmt.Sprintf("Blocks of type %q are not expected here.%s", blockTy, suggestion),
|
|
||||||
Subject: &block.TypeRange,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return content, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
|
|
||||||
attrs := make(hcl.Attributes)
|
|
||||||
var blocks hcl.Blocks
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
hiddenAttrs := make(map[string]struct{})
|
|
||||||
hiddenBlocks := make(map[string]struct{})
|
|
||||||
|
|
||||||
if b.hiddenAttrs != nil {
|
|
||||||
for k, v := range b.hiddenAttrs {
|
|
||||||
hiddenAttrs[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if b.hiddenBlocks != nil {
|
|
||||||
for k, v := range b.hiddenBlocks {
|
|
||||||
hiddenBlocks[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, attrS := range schema.Attributes {
|
|
||||||
name := attrS.Name
|
|
||||||
attr, exists := b.Attributes[name]
|
|
||||||
_, hidden := hiddenAttrs[name]
|
|
||||||
if hidden || !exists {
|
|
||||||
if attrS.Required {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Missing required argument",
|
|
||||||
Detail: fmt.Sprintf("The argument %q is required, but no definition was found.", attrS.Name),
|
|
||||||
Subject: b.MissingItemRange().Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
hiddenAttrs[name] = struct{}{}
|
|
||||||
attrs[name] = attr.AsHCLAttribute()
|
|
||||||
}
|
|
||||||
|
|
||||||
blocksWanted := make(map[string]hcl.BlockHeaderSchema)
|
|
||||||
for _, blockS := range schema.Blocks {
|
|
||||||
blocksWanted[blockS.Type] = blockS
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, block := range b.Blocks {
|
|
||||||
if _, hidden := hiddenBlocks[block.Type]; hidden {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
blockS, wanted := blocksWanted[block.Type]
|
|
||||||
if !wanted {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(block.Labels) > len(blockS.LabelNames) {
|
|
||||||
name := block.Type
|
|
||||||
if len(blockS.LabelNames) == 0 {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: fmt.Sprintf("Extraneous label for %s", name),
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"No labels are expected for %s blocks.", name,
|
|
||||||
),
|
|
||||||
Subject: block.LabelRanges[0].Ptr(),
|
|
||||||
Context: hcl.RangeBetween(block.TypeRange, block.OpenBraceRange).Ptr(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: fmt.Sprintf("Extraneous label for %s", name),
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"Only %d labels (%s) are expected for %s blocks.",
|
|
||||||
len(blockS.LabelNames), strings.Join(blockS.LabelNames, ", "), name,
|
|
||||||
),
|
|
||||||
Subject: block.LabelRanges[len(blockS.LabelNames)].Ptr(),
|
|
||||||
Context: hcl.RangeBetween(block.TypeRange, block.OpenBraceRange).Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(block.Labels) < len(blockS.LabelNames) {
|
|
||||||
name := block.Type
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: fmt.Sprintf("Missing %s for %s", blockS.LabelNames[len(block.Labels)], name),
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"All %s blocks must have %d labels (%s).",
|
|
||||||
name, len(blockS.LabelNames), strings.Join(blockS.LabelNames, ", "),
|
|
||||||
),
|
|
||||||
Subject: &block.OpenBraceRange,
|
|
||||||
Context: hcl.RangeBetween(block.TypeRange, block.OpenBraceRange).Ptr(),
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
blocks = append(blocks, block.AsHCLBlock())
|
|
||||||
}
|
|
||||||
|
|
||||||
// We hide blocks only after we've processed all of them, since otherwise
|
|
||||||
// we can't process more than one of the same type.
|
|
||||||
for _, blockS := range schema.Blocks {
|
|
||||||
hiddenBlocks[blockS.Type] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
remain := &Body{
|
|
||||||
Attributes: b.Attributes,
|
|
||||||
Blocks: b.Blocks,
|
|
||||||
|
|
||||||
hiddenAttrs: hiddenAttrs,
|
|
||||||
hiddenBlocks: hiddenBlocks,
|
|
||||||
|
|
||||||
SrcRange: b.SrcRange,
|
|
||||||
EndRange: b.EndRange,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &hcl.BodyContent{
|
|
||||||
Attributes: attrs,
|
|
||||||
Blocks: blocks,
|
|
||||||
|
|
||||||
MissingItemRange: b.MissingItemRange(),
|
|
||||||
}, remain, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Body) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
|
|
||||||
attrs := make(hcl.Attributes)
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
|
|
||||||
if len(b.Blocks) > 0 {
|
|
||||||
example := b.Blocks[0]
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: fmt.Sprintf("Unexpected %q block", example.Type),
|
|
||||||
Detail: "Blocks are not allowed here.",
|
|
||||||
Subject: &example.TypeRange,
|
|
||||||
})
|
|
||||||
// we will continue processing anyway, and return the attributes
|
|
||||||
// we are able to find so that certain analyses can still be done
|
|
||||||
// in the face of errors.
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.Attributes == nil {
|
|
||||||
return attrs, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, attr := range b.Attributes {
|
|
||||||
if _, hidden := b.hiddenAttrs[name]; hidden {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
attrs[name] = attr.AsHCLAttribute()
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Body) MissingItemRange() hcl.Range {
|
|
||||||
return hcl.Range{
|
|
||||||
Filename: b.SrcRange.Filename,
|
|
||||||
Start: b.SrcRange.Start,
|
|
||||||
End: b.SrcRange.Start,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attributes is the collection of attribute definitions within a body.
|
|
||||||
type Attributes map[string]*Attribute
|
|
||||||
|
|
||||||
func (a Attributes) walkChildNodes(w internalWalkFunc) {
|
|
||||||
for _, attr := range a {
|
|
||||||
w(attr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Range returns the range of some arbitrary point within the set of
|
|
||||||
// attributes, or an invalid range if there are no attributes.
|
|
||||||
//
|
|
||||||
// This is provided only to complete the Node interface, but has no practical
|
|
||||||
// use.
|
|
||||||
func (a Attributes) Range() hcl.Range {
|
|
||||||
// An attributes doesn't really have a useful range to report, since
|
|
||||||
// it's just a grouping construct. So we'll arbitrarily take the
|
|
||||||
// range of one of the attributes, or produce an invalid range if we have
|
|
||||||
// none. In practice, there's little reason to ask for the range of
|
|
||||||
// an Attributes.
|
|
||||||
for _, attr := range a {
|
|
||||||
return attr.Range()
|
|
||||||
}
|
|
||||||
return hcl.Range{
|
|
||||||
Filename: "<unknown>",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attribute represents a single attribute definition within a body.
|
|
||||||
type Attribute struct {
|
|
||||||
Name string
|
|
||||||
Expr Expression
|
|
||||||
|
|
||||||
SrcRange hcl.Range
|
|
||||||
NameRange hcl.Range
|
|
||||||
EqualsRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Attribute) walkChildNodes(w internalWalkFunc) {
|
|
||||||
w(a.Expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Attribute) Range() hcl.Range {
|
|
||||||
return a.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
// AsHCLAttribute returns the block data expressed as a *hcl.Attribute.
|
|
||||||
func (a *Attribute) AsHCLAttribute() *hcl.Attribute {
|
|
||||||
if a == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &hcl.Attribute{
|
|
||||||
Name: a.Name,
|
|
||||||
Expr: a.Expr,
|
|
||||||
|
|
||||||
Range: a.SrcRange,
|
|
||||||
NameRange: a.NameRange,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blocks is the list of nested blocks within a body.
|
|
||||||
type Blocks []*Block
|
|
||||||
|
|
||||||
func (bs Blocks) walkChildNodes(w internalWalkFunc) {
|
|
||||||
for _, block := range bs {
|
|
||||||
w(block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Range returns the range of some arbitrary point within the list of
|
|
||||||
// blocks, or an invalid range if there are no blocks.
|
|
||||||
//
|
|
||||||
// This is provided only to complete the Node interface, but has no practical
|
|
||||||
// use.
|
|
||||||
func (bs Blocks) Range() hcl.Range {
|
|
||||||
if len(bs) > 0 {
|
|
||||||
return bs[0].Range()
|
|
||||||
}
|
|
||||||
return hcl.Range{
|
|
||||||
Filename: "<unknown>",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block represents a nested block structure
|
|
||||||
type Block struct {
|
|
||||||
Type string
|
|
||||||
Labels []string
|
|
||||||
Body *Body
|
|
||||||
|
|
||||||
TypeRange hcl.Range
|
|
||||||
LabelRanges []hcl.Range
|
|
||||||
OpenBraceRange hcl.Range
|
|
||||||
CloseBraceRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Block) walkChildNodes(w internalWalkFunc) {
|
|
||||||
w(b.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Block) Range() hcl.Range {
|
|
||||||
return hcl.RangeBetween(b.TypeRange, b.CloseBraceRange)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Block) DefRange() hcl.Range {
|
|
||||||
return hcl.RangeBetween(b.TypeRange, b.OpenBraceRange)
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// The methods in this file are all optional extension methods that serve to
|
|
||||||
// implement the methods of the same name on *hcl.File when its root body
|
|
||||||
// is provided by this package.
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// BlocksAtPos implements the method of the same name for an *hcl.File that
|
|
||||||
// is backed by a *Body.
|
|
||||||
func (b *Body) BlocksAtPos(pos hcl.Pos) []*hcl.Block {
|
|
||||||
list, _ := b.blocksAtPos(pos, true)
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
// InnermostBlockAtPos implements the method of the same name for an *hcl.File
|
|
||||||
// that is backed by a *Body.
|
|
||||||
func (b *Body) InnermostBlockAtPos(pos hcl.Pos) *hcl.Block {
|
|
||||||
_, innermost := b.blocksAtPos(pos, false)
|
|
||||||
return innermost.AsHCLBlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutermostBlockAtPos implements the method of the same name for an *hcl.File
|
|
||||||
// that is backed by a *Body.
|
|
||||||
func (b *Body) OutermostBlockAtPos(pos hcl.Pos) *hcl.Block {
|
|
||||||
return b.outermostBlockAtPos(pos).AsHCLBlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// blocksAtPos is the internal engine of both BlocksAtPos and
|
|
||||||
// InnermostBlockAtPos, which both need to do the same logic but return a
|
|
||||||
// differently-shaped result.
|
|
||||||
//
|
|
||||||
// list is nil if makeList is false, avoiding an allocation. Innermost is
|
|
||||||
// always set, and if the returned list is non-nil it will always match the
|
|
||||||
// final element from that list.
|
|
||||||
func (b *Body) blocksAtPos(pos hcl.Pos, makeList bool) (list []*hcl.Block, innermost *Block) {
|
|
||||||
current := b
|
|
||||||
|
|
||||||
Blocks:
|
|
||||||
for current != nil {
|
|
||||||
for _, block := range current.Blocks {
|
|
||||||
wholeRange := hcl.RangeBetween(block.TypeRange, block.CloseBraceRange)
|
|
||||||
if wholeRange.ContainsPos(pos) {
|
|
||||||
innermost = block
|
|
||||||
if makeList {
|
|
||||||
list = append(list, innermost.AsHCLBlock())
|
|
||||||
}
|
|
||||||
current = block.Body
|
|
||||||
continue Blocks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we fall out here then none of the current body's nested blocks
|
|
||||||
// contain the position we are looking for, and so we're done.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// outermostBlockAtPos is the internal version of OutermostBlockAtPos that
|
|
||||||
// returns a hclsyntax.Block rather than an hcl.Block, allowing for further
|
|
||||||
// analysis if necessary.
|
|
||||||
func (b *Body) outermostBlockAtPos(pos hcl.Pos) *Block {
|
|
||||||
// This is similar to blocksAtPos, but simpler because we know it only
|
|
||||||
// ever needs to search the first level of nested blocks.
|
|
||||||
|
|
||||||
for _, block := range b.Blocks {
|
|
||||||
wholeRange := hcl.RangeBetween(block.TypeRange, block.CloseBraceRange)
|
|
||||||
if wholeRange.ContainsPos(pos) {
|
|
||||||
return block
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AttributeAtPos implements the method of the same name for an *hcl.File
|
|
||||||
// that is backed by a *Body.
|
|
||||||
func (b *Body) AttributeAtPos(pos hcl.Pos) *hcl.Attribute {
|
|
||||||
return b.attributeAtPos(pos).AsHCLAttribute()
|
|
||||||
}
|
|
||||||
|
|
||||||
// attributeAtPos is the internal version of AttributeAtPos that returns a
|
|
||||||
// hclsyntax.Block rather than an hcl.Block, allowing for further analysis if
|
|
||||||
// necessary.
|
|
||||||
func (b *Body) attributeAtPos(pos hcl.Pos) *Attribute {
|
|
||||||
searchBody := b
|
|
||||||
_, block := b.blocksAtPos(pos, false)
|
|
||||||
if block != nil {
|
|
||||||
searchBody = block.Body
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, attr := range searchBody.Attributes {
|
|
||||||
if attr.SrcRange.ContainsPos(pos) {
|
|
||||||
return attr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutermostExprAtPos implements the method of the same name for an *hcl.File
|
|
||||||
// that is backed by a *Body.
|
|
||||||
func (b *Body) OutermostExprAtPos(pos hcl.Pos) hcl.Expression {
|
|
||||||
attr := b.attributeAtPos(pos)
|
|
||||||
if attr == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !attr.Expr.Range().ContainsPos(pos) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return attr.Expr
|
|
||||||
}
|
|
|
@ -1,320 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/apparentlymart/go-textseg/textseg"
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Token represents a sequence of bytes from some HCL code that has been
|
|
||||||
// tagged with a type and its range within the source file.
|
|
||||||
type Token struct {
|
|
||||||
Type TokenType
|
|
||||||
Bytes []byte
|
|
||||||
Range hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tokens is a slice of Token.
|
|
||||||
type Tokens []Token
|
|
||||||
|
|
||||||
// TokenType is an enumeration used for the Type field on Token.
|
|
||||||
type TokenType rune
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Single-character tokens are represented by their own character, for
|
|
||||||
// convenience in producing these within the scanner. However, the values
|
|
||||||
// are otherwise arbitrary and just intended to be mnemonic for humans
|
|
||||||
// who might see them in debug output.
|
|
||||||
|
|
||||||
TokenOBrace TokenType = '{'
|
|
||||||
TokenCBrace TokenType = '}'
|
|
||||||
TokenOBrack TokenType = '['
|
|
||||||
TokenCBrack TokenType = ']'
|
|
||||||
TokenOParen TokenType = '('
|
|
||||||
TokenCParen TokenType = ')'
|
|
||||||
TokenOQuote TokenType = '«'
|
|
||||||
TokenCQuote TokenType = '»'
|
|
||||||
TokenOHeredoc TokenType = 'H'
|
|
||||||
TokenCHeredoc TokenType = 'h'
|
|
||||||
|
|
||||||
TokenStar TokenType = '*'
|
|
||||||
TokenSlash TokenType = '/'
|
|
||||||
TokenPlus TokenType = '+'
|
|
||||||
TokenMinus TokenType = '-'
|
|
||||||
TokenPercent TokenType = '%'
|
|
||||||
|
|
||||||
TokenEqual TokenType = '='
|
|
||||||
TokenEqualOp TokenType = '≔'
|
|
||||||
TokenNotEqual TokenType = '≠'
|
|
||||||
TokenLessThan TokenType = '<'
|
|
||||||
TokenLessThanEq TokenType = '≤'
|
|
||||||
TokenGreaterThan TokenType = '>'
|
|
||||||
TokenGreaterThanEq TokenType = '≥'
|
|
||||||
|
|
||||||
TokenAnd TokenType = '∧'
|
|
||||||
TokenOr TokenType = '∨'
|
|
||||||
TokenBang TokenType = '!'
|
|
||||||
|
|
||||||
TokenDot TokenType = '.'
|
|
||||||
TokenComma TokenType = ','
|
|
||||||
|
|
||||||
TokenEllipsis TokenType = '…'
|
|
||||||
TokenFatArrow TokenType = '⇒'
|
|
||||||
|
|
||||||
TokenQuestion TokenType = '?'
|
|
||||||
TokenColon TokenType = ':'
|
|
||||||
|
|
||||||
TokenTemplateInterp TokenType = '∫'
|
|
||||||
TokenTemplateControl TokenType = 'λ'
|
|
||||||
TokenTemplateSeqEnd TokenType = '∎'
|
|
||||||
|
|
||||||
TokenQuotedLit TokenType = 'Q' // might contain backslash escapes
|
|
||||||
TokenStringLit TokenType = 'S' // cannot contain backslash escapes
|
|
||||||
TokenNumberLit TokenType = 'N'
|
|
||||||
TokenIdent TokenType = 'I'
|
|
||||||
|
|
||||||
TokenComment TokenType = 'C'
|
|
||||||
|
|
||||||
TokenNewline TokenType = '\n'
|
|
||||||
TokenEOF TokenType = '␄'
|
|
||||||
|
|
||||||
// The rest are not used in the language but recognized by the scanner so
|
|
||||||
// we can generate good diagnostics in the parser when users try to write
|
|
||||||
// things that might work in other languages they are familiar with, or
|
|
||||||
// simply make incorrect assumptions about the HCL language.
|
|
||||||
|
|
||||||
TokenBitwiseAnd TokenType = '&'
|
|
||||||
TokenBitwiseOr TokenType = '|'
|
|
||||||
TokenBitwiseNot TokenType = '~'
|
|
||||||
TokenBitwiseXor TokenType = '^'
|
|
||||||
TokenStarStar TokenType = '➚'
|
|
||||||
TokenApostrophe TokenType = '\''
|
|
||||||
TokenBacktick TokenType = '`'
|
|
||||||
TokenSemicolon TokenType = ';'
|
|
||||||
TokenTabs TokenType = '␉'
|
|
||||||
TokenInvalid TokenType = '<27>'
|
|
||||||
TokenBadUTF8 TokenType = '💩'
|
|
||||||
TokenQuotedNewline TokenType = ''
|
|
||||||
|
|
||||||
// TokenNil is a placeholder for when a token is required but none is
|
|
||||||
// available, e.g. when reporting errors. The scanner will never produce
|
|
||||||
// this as part of a token stream.
|
|
||||||
TokenNil TokenType = '\x00'
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t TokenType) GoString() string {
|
|
||||||
return fmt.Sprintf("hclsyntax.%s", t.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
type scanMode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
scanNormal scanMode = iota
|
|
||||||
scanTemplate
|
|
||||||
scanIdentOnly
|
|
||||||
)
|
|
||||||
|
|
||||||
type tokenAccum struct {
|
|
||||||
Filename string
|
|
||||||
Bytes []byte
|
|
||||||
Pos hcl.Pos
|
|
||||||
Tokens []Token
|
|
||||||
StartByte int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *tokenAccum) emitToken(ty TokenType, startOfs, endOfs int) {
|
|
||||||
// Walk through our buffer to figure out how much we need to adjust
|
|
||||||
// the start pos to get our end pos.
|
|
||||||
|
|
||||||
start := f.Pos
|
|
||||||
start.Column += startOfs + f.StartByte - f.Pos.Byte // Safe because only ASCII spaces can be in the offset
|
|
||||||
start.Byte = startOfs + f.StartByte
|
|
||||||
|
|
||||||
end := start
|
|
||||||
end.Byte = endOfs + f.StartByte
|
|
||||||
b := f.Bytes[startOfs:endOfs]
|
|
||||||
for len(b) > 0 {
|
|
||||||
advance, seq, _ := textseg.ScanGraphemeClusters(b, true)
|
|
||||||
if (len(seq) == 1 && seq[0] == '\n') || (len(seq) == 2 && seq[0] == '\r' && seq[1] == '\n') {
|
|
||||||
end.Line++
|
|
||||||
end.Column = 1
|
|
||||||
} else {
|
|
||||||
end.Column++
|
|
||||||
}
|
|
||||||
b = b[advance:]
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Pos = end
|
|
||||||
|
|
||||||
f.Tokens = append(f.Tokens, Token{
|
|
||||||
Type: ty,
|
|
||||||
Bytes: f.Bytes[startOfs:endOfs],
|
|
||||||
Range: hcl.Range{
|
|
||||||
Filename: f.Filename,
|
|
||||||
Start: start,
|
|
||||||
End: end,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type heredocInProgress struct {
|
|
||||||
Marker []byte
|
|
||||||
StartOfLine bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func tokenOpensFlushHeredoc(tok Token) bool {
|
|
||||||
if tok.Type != TokenOHeredoc {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return bytes.HasPrefix(tok.Bytes, []byte{'<', '<', '-'})
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkInvalidTokens does a simple pass across the given tokens and generates
|
|
||||||
// diagnostics for tokens that should _never_ appear in HCL source. This
|
|
||||||
// is intended to avoid the need for the parser to have special support
|
|
||||||
// for them all over.
|
|
||||||
//
|
|
||||||
// Returns a diagnostics with no errors if everything seems acceptable.
|
|
||||||
// Otherwise, returns zero or more error diagnostics, though tries to limit
|
|
||||||
// repetition of the same information.
|
|
||||||
func checkInvalidTokens(tokens Tokens) hcl.Diagnostics {
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
|
|
||||||
toldBitwise := 0
|
|
||||||
toldExponent := 0
|
|
||||||
toldBacktick := 0
|
|
||||||
toldApostrophe := 0
|
|
||||||
toldSemicolon := 0
|
|
||||||
toldTabs := 0
|
|
||||||
toldBadUTF8 := 0
|
|
||||||
|
|
||||||
for _, tok := range tokens {
|
|
||||||
// copy token so it's safe to point to it
|
|
||||||
tok := tok
|
|
||||||
|
|
||||||
switch tok.Type {
|
|
||||||
case TokenBitwiseAnd, TokenBitwiseOr, TokenBitwiseXor, TokenBitwiseNot:
|
|
||||||
if toldBitwise < 4 {
|
|
||||||
var suggestion string
|
|
||||||
switch tok.Type {
|
|
||||||
case TokenBitwiseAnd:
|
|
||||||
suggestion = " Did you mean boolean AND (\"&&\")?"
|
|
||||||
case TokenBitwiseOr:
|
|
||||||
suggestion = " Did you mean boolean OR (\"&&\")?"
|
|
||||||
case TokenBitwiseNot:
|
|
||||||
suggestion = " Did you mean boolean NOT (\"!\")?"
|
|
||||||
}
|
|
||||||
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unsupported operator",
|
|
||||||
Detail: fmt.Sprintf("Bitwise operators are not supported.%s", suggestion),
|
|
||||||
Subject: &tok.Range,
|
|
||||||
})
|
|
||||||
toldBitwise++
|
|
||||||
}
|
|
||||||
case TokenStarStar:
|
|
||||||
if toldExponent < 1 {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unsupported operator",
|
|
||||||
Detail: "\"**\" is not a supported operator. Exponentiation is not supported as an operator.",
|
|
||||||
Subject: &tok.Range,
|
|
||||||
})
|
|
||||||
|
|
||||||
toldExponent++
|
|
||||||
}
|
|
||||||
case TokenBacktick:
|
|
||||||
// Only report for alternating (even) backticks, so we won't report both start and ends of the same
|
|
||||||
// backtick-quoted string.
|
|
||||||
if (toldBacktick % 2) == 0 {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid character",
|
|
||||||
Detail: "The \"`\" character is not valid. To create a multi-line string, use the \"heredoc\" syntax, like \"<<EOT\".",
|
|
||||||
Subject: &tok.Range,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if toldBacktick <= 2 {
|
|
||||||
toldBacktick++
|
|
||||||
}
|
|
||||||
case TokenApostrophe:
|
|
||||||
if (toldApostrophe % 2) == 0 {
|
|
||||||
newDiag := &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid character",
|
|
||||||
Detail: "Single quotes are not valid. Use double quotes (\") to enclose strings.",
|
|
||||||
Subject: &tok.Range,
|
|
||||||
}
|
|
||||||
diags = append(diags, newDiag)
|
|
||||||
}
|
|
||||||
if toldApostrophe <= 2 {
|
|
||||||
toldApostrophe++
|
|
||||||
}
|
|
||||||
case TokenSemicolon:
|
|
||||||
if toldSemicolon < 1 {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid character",
|
|
||||||
Detail: "The \";\" character is not valid. Use newlines to separate arguments and blocks, and commas to separate items in collection values.",
|
|
||||||
Subject: &tok.Range,
|
|
||||||
})
|
|
||||||
|
|
||||||
toldSemicolon++
|
|
||||||
}
|
|
||||||
case TokenTabs:
|
|
||||||
if toldTabs < 1 {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid character",
|
|
||||||
Detail: "Tab characters may not be used. The recommended indentation style is two spaces per indent.",
|
|
||||||
Subject: &tok.Range,
|
|
||||||
})
|
|
||||||
|
|
||||||
toldTabs++
|
|
||||||
}
|
|
||||||
case TokenBadUTF8:
|
|
||||||
if toldBadUTF8 < 1 {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid character encoding",
|
|
||||||
Detail: "All input files must be UTF-8 encoded. Ensure that UTF-8 encoding is selected in your editor.",
|
|
||||||
Subject: &tok.Range,
|
|
||||||
})
|
|
||||||
|
|
||||||
toldBadUTF8++
|
|
||||||
}
|
|
||||||
case TokenQuotedNewline:
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid multi-line string",
|
|
||||||
Detail: "Quoted strings may not be split over multiple lines. To produce a multi-line string, either use the \\n escape to represent a newline character or use the \"heredoc\" multi-line template syntax.",
|
|
||||||
Subject: &tok.Range,
|
|
||||||
})
|
|
||||||
case TokenInvalid:
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid character",
|
|
||||||
Detail: "This character is not used within the language.",
|
|
||||||
Subject: &tok.Range,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
var utf8BOM = []byte{0xef, 0xbb, 0xbf}
|
|
||||||
|
|
||||||
// stripUTF8BOM checks whether the given buffer begins with a UTF-8 byte order
|
|
||||||
// mark (0xEF 0xBB 0xBF) and, if so, returns a truncated slice with the same
|
|
||||||
// backing array but with the BOM skipped.
|
|
||||||
//
|
|
||||||
// If there is no BOM present, the given slice is returned verbatim.
|
|
||||||
func stripUTF8BOM(src []byte) []byte {
|
|
||||||
if bytes.HasPrefix(src, utf8BOM) {
|
|
||||||
return src[3:]
|
|
||||||
}
|
|
||||||
return src
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
// Code generated by "stringer -type TokenType -output token_type_string.go"; DO NOT EDIT.
|
|
||||||
|
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import "strconv"
|
|
||||||
|
|
||||||
func _() {
|
|
||||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
|
||||||
// Re-run the stringer command to generate them again.
|
|
||||||
var x [1]struct{}
|
|
||||||
_ = x[TokenOBrace-123]
|
|
||||||
_ = x[TokenCBrace-125]
|
|
||||||
_ = x[TokenOBrack-91]
|
|
||||||
_ = x[TokenCBrack-93]
|
|
||||||
_ = x[TokenOParen-40]
|
|
||||||
_ = x[TokenCParen-41]
|
|
||||||
_ = x[TokenOQuote-171]
|
|
||||||
_ = x[TokenCQuote-187]
|
|
||||||
_ = x[TokenOHeredoc-72]
|
|
||||||
_ = x[TokenCHeredoc-104]
|
|
||||||
_ = x[TokenStar-42]
|
|
||||||
_ = x[TokenSlash-47]
|
|
||||||
_ = x[TokenPlus-43]
|
|
||||||
_ = x[TokenMinus-45]
|
|
||||||
_ = x[TokenPercent-37]
|
|
||||||
_ = x[TokenEqual-61]
|
|
||||||
_ = x[TokenEqualOp-8788]
|
|
||||||
_ = x[TokenNotEqual-8800]
|
|
||||||
_ = x[TokenLessThan-60]
|
|
||||||
_ = x[TokenLessThanEq-8804]
|
|
||||||
_ = x[TokenGreaterThan-62]
|
|
||||||
_ = x[TokenGreaterThanEq-8805]
|
|
||||||
_ = x[TokenAnd-8743]
|
|
||||||
_ = x[TokenOr-8744]
|
|
||||||
_ = x[TokenBang-33]
|
|
||||||
_ = x[TokenDot-46]
|
|
||||||
_ = x[TokenComma-44]
|
|
||||||
_ = x[TokenEllipsis-8230]
|
|
||||||
_ = x[TokenFatArrow-8658]
|
|
||||||
_ = x[TokenQuestion-63]
|
|
||||||
_ = x[TokenColon-58]
|
|
||||||
_ = x[TokenTemplateInterp-8747]
|
|
||||||
_ = x[TokenTemplateControl-955]
|
|
||||||
_ = x[TokenTemplateSeqEnd-8718]
|
|
||||||
_ = x[TokenQuotedLit-81]
|
|
||||||
_ = x[TokenStringLit-83]
|
|
||||||
_ = x[TokenNumberLit-78]
|
|
||||||
_ = x[TokenIdent-73]
|
|
||||||
_ = x[TokenComment-67]
|
|
||||||
_ = x[TokenNewline-10]
|
|
||||||
_ = x[TokenEOF-9220]
|
|
||||||
_ = x[TokenBitwiseAnd-38]
|
|
||||||
_ = x[TokenBitwiseOr-124]
|
|
||||||
_ = x[TokenBitwiseNot-126]
|
|
||||||
_ = x[TokenBitwiseXor-94]
|
|
||||||
_ = x[TokenStarStar-10138]
|
|
||||||
_ = x[TokenApostrophe-39]
|
|
||||||
_ = x[TokenBacktick-96]
|
|
||||||
_ = x[TokenSemicolon-59]
|
|
||||||
_ = x[TokenTabs-9225]
|
|
||||||
_ = x[TokenInvalid-65533]
|
|
||||||
_ = x[TokenBadUTF8-128169]
|
|
||||||
_ = x[TokenQuotedNewline-9252]
|
|
||||||
_ = x[TokenNil-0]
|
|
||||||
}
|
|
||||||
|
|
||||||
const _TokenType_name = "TokenNilTokenNewlineTokenBangTokenPercentTokenBitwiseAndTokenApostropheTokenOParenTokenCParenTokenStarTokenPlusTokenCommaTokenMinusTokenDotTokenSlashTokenColonTokenSemicolonTokenLessThanTokenEqualTokenGreaterThanTokenQuestionTokenCommentTokenOHeredocTokenIdentTokenNumberLitTokenQuotedLitTokenStringLitTokenOBrackTokenCBrackTokenBitwiseXorTokenBacktickTokenCHeredocTokenOBraceTokenBitwiseOrTokenCBraceTokenBitwiseNotTokenOQuoteTokenCQuoteTokenTemplateControlTokenEllipsisTokenFatArrowTokenTemplateSeqEndTokenAndTokenOrTokenTemplateInterpTokenEqualOpTokenNotEqualTokenLessThanEqTokenGreaterThanEqTokenEOFTokenTabsTokenQuotedNewlineTokenStarStarTokenInvalidTokenBadUTF8"
|
|
||||||
|
|
||||||
var _TokenType_map = map[TokenType]string{
|
|
||||||
0: _TokenType_name[0:8],
|
|
||||||
10: _TokenType_name[8:20],
|
|
||||||
33: _TokenType_name[20:29],
|
|
||||||
37: _TokenType_name[29:41],
|
|
||||||
38: _TokenType_name[41:56],
|
|
||||||
39: _TokenType_name[56:71],
|
|
||||||
40: _TokenType_name[71:82],
|
|
||||||
41: _TokenType_name[82:93],
|
|
||||||
42: _TokenType_name[93:102],
|
|
||||||
43: _TokenType_name[102:111],
|
|
||||||
44: _TokenType_name[111:121],
|
|
||||||
45: _TokenType_name[121:131],
|
|
||||||
46: _TokenType_name[131:139],
|
|
||||||
47: _TokenType_name[139:149],
|
|
||||||
58: _TokenType_name[149:159],
|
|
||||||
59: _TokenType_name[159:173],
|
|
||||||
60: _TokenType_name[173:186],
|
|
||||||
61: _TokenType_name[186:196],
|
|
||||||
62: _TokenType_name[196:212],
|
|
||||||
63: _TokenType_name[212:225],
|
|
||||||
67: _TokenType_name[225:237],
|
|
||||||
72: _TokenType_name[237:250],
|
|
||||||
73: _TokenType_name[250:260],
|
|
||||||
78: _TokenType_name[260:274],
|
|
||||||
81: _TokenType_name[274:288],
|
|
||||||
83: _TokenType_name[288:302],
|
|
||||||
91: _TokenType_name[302:313],
|
|
||||||
93: _TokenType_name[313:324],
|
|
||||||
94: _TokenType_name[324:339],
|
|
||||||
96: _TokenType_name[339:352],
|
|
||||||
104: _TokenType_name[352:365],
|
|
||||||
123: _TokenType_name[365:376],
|
|
||||||
124: _TokenType_name[376:390],
|
|
||||||
125: _TokenType_name[390:401],
|
|
||||||
126: _TokenType_name[401:416],
|
|
||||||
171: _TokenType_name[416:427],
|
|
||||||
187: _TokenType_name[427:438],
|
|
||||||
955: _TokenType_name[438:458],
|
|
||||||
8230: _TokenType_name[458:471],
|
|
||||||
8658: _TokenType_name[471:484],
|
|
||||||
8718: _TokenType_name[484:503],
|
|
||||||
8743: _TokenType_name[503:511],
|
|
||||||
8744: _TokenType_name[511:518],
|
|
||||||
8747: _TokenType_name[518:537],
|
|
||||||
8788: _TokenType_name[537:549],
|
|
||||||
8800: _TokenType_name[549:562],
|
|
||||||
8804: _TokenType_name[562:577],
|
|
||||||
8805: _TokenType_name[577:595],
|
|
||||||
9220: _TokenType_name[595:603],
|
|
||||||
9225: _TokenType_name[603:612],
|
|
||||||
9252: _TokenType_name[612:630],
|
|
||||||
10138: _TokenType_name[630:643],
|
|
||||||
65533: _TokenType_name[643:655],
|
|
||||||
128169: _TokenType_name[655:667],
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i TokenType) String() string {
|
|
||||||
if str, ok := _TokenType_map[i]; ok {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
return "TokenType(" + strconv.FormatInt(int64(i), 10) + ")"
|
|
||||||
}
|
|
|
@ -1,335 +0,0 @@
|
||||||
#!/usr/bin/env ruby
|
|
||||||
#
|
|
||||||
# This scripted has been updated to accept more command-line arguments:
|
|
||||||
#
|
|
||||||
# -u, --url URL to process
|
|
||||||
# -m, --machine Machine name
|
|
||||||
# -p, --properties Properties to add to the machine
|
|
||||||
# -o, --output Write output to file
|
|
||||||
#
|
|
||||||
# Updated by: Marty Schoch <marty.schoch@gmail.com>
|
|
||||||
#
|
|
||||||
# This script uses the unicode spec to generate a Ragel state machine
|
|
||||||
# that recognizes unicode alphanumeric characters. It generates 5
|
|
||||||
# character classes: uupper, ulower, ualpha, udigit, and ualnum.
|
|
||||||
# Currently supported encodings are UTF-8 [default] and UCS-4.
|
|
||||||
#
|
|
||||||
# Usage: unicode2ragel.rb [options]
|
|
||||||
# -e, --encoding [ucs4 | utf8] Data encoding
|
|
||||||
# -h, --help Show this message
|
|
||||||
#
|
|
||||||
# This script was originally written as part of the Ferret search
|
|
||||||
# engine library.
|
|
||||||
#
|
|
||||||
# Author: Rakan El-Khalil <rakan@well.com>
|
|
||||||
|
|
||||||
require 'optparse'
|
|
||||||
require 'open-uri'
|
|
||||||
|
|
||||||
ENCODINGS = [ :utf8, :ucs4 ]
|
|
||||||
ALPHTYPES = { :utf8 => "byte", :ucs4 => "rune" }
|
|
||||||
DEFAULT_CHART_URL = "http://www.unicode.org/Public/5.1.0/ucd/DerivedCoreProperties.txt"
|
|
||||||
DEFAULT_MACHINE_NAME= "WChar"
|
|
||||||
|
|
||||||
###
|
|
||||||
# Display vars & default option
|
|
||||||
|
|
||||||
TOTAL_WIDTH = 80
|
|
||||||
RANGE_WIDTH = 23
|
|
||||||
@encoding = :utf8
|
|
||||||
@chart_url = DEFAULT_CHART_URL
|
|
||||||
machine_name = DEFAULT_MACHINE_NAME
|
|
||||||
properties = []
|
|
||||||
@output = $stdout
|
|
||||||
|
|
||||||
###
|
|
||||||
# Option parsing
|
|
||||||
|
|
||||||
cli_opts = OptionParser.new do |opts|
|
|
||||||
opts.on("-e", "--encoding [ucs4 | utf8]", "Data encoding") do |o|
|
|
||||||
@encoding = o.downcase.to_sym
|
|
||||||
end
|
|
||||||
opts.on("-h", "--help", "Show this message") do
|
|
||||||
puts opts
|
|
||||||
exit
|
|
||||||
end
|
|
||||||
opts.on("-u", "--url URL", "URL to process") do |o|
|
|
||||||
@chart_url = o
|
|
||||||
end
|
|
||||||
opts.on("-m", "--machine MACHINE_NAME", "Machine name") do |o|
|
|
||||||
machine_name = o
|
|
||||||
end
|
|
||||||
opts.on("-p", "--properties x,y,z", Array, "Properties to add to machine") do |o|
|
|
||||||
properties = o
|
|
||||||
end
|
|
||||||
opts.on("-o", "--output FILE", "output file") do |o|
|
|
||||||
@output = File.new(o, "w+")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
cli_opts.parse(ARGV)
|
|
||||||
unless ENCODINGS.member? @encoding
|
|
||||||
puts "Invalid encoding: #{@encoding}"
|
|
||||||
puts cli_opts
|
|
||||||
exit
|
|
||||||
end
|
|
||||||
|
|
||||||
##
|
|
||||||
# Downloads the document at url and yields every alpha line's hex
|
|
||||||
# range and description.
|
|
||||||
|
|
||||||
def each_alpha( url, property )
|
|
||||||
open( url ) do |file|
|
|
||||||
file.each_line do |line|
|
|
||||||
next if line =~ /^#/;
|
|
||||||
next if line !~ /; #{property} #/;
|
|
||||||
|
|
||||||
range, description = line.split(/;/)
|
|
||||||
range.strip!
|
|
||||||
description.gsub!(/.*#/, '').strip!
|
|
||||||
|
|
||||||
if range =~ /\.\./
|
|
||||||
start, stop = range.split '..'
|
|
||||||
else start = stop = range
|
|
||||||
end
|
|
||||||
|
|
||||||
yield start.hex .. stop.hex, description
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
###
|
|
||||||
# Formats to hex at minimum width
|
|
||||||
|
|
||||||
def to_hex( n )
|
|
||||||
r = "%0X" % n
|
|
||||||
r = "0#{r}" unless (r.length % 2).zero?
|
|
||||||
r
|
|
||||||
end
|
|
||||||
|
|
||||||
###
|
|
||||||
# UCS4 is just a straight hex conversion of the unicode codepoint.
|
|
||||||
|
|
||||||
def to_ucs4( range )
|
|
||||||
rangestr = "0x" + to_hex(range.begin)
|
|
||||||
rangestr << "..0x" + to_hex(range.end) if range.begin != range.end
|
|
||||||
[ rangestr ]
|
|
||||||
end
|
|
||||||
|
|
||||||
##
|
|
||||||
# 0x00 - 0x7f -> 0zzzzzzz[7]
|
|
||||||
# 0x80 - 0x7ff -> 110yyyyy[5] 10zzzzzz[6]
|
|
||||||
# 0x800 - 0xffff -> 1110xxxx[4] 10yyyyyy[6] 10zzzzzz[6]
|
|
||||||
# 0x010000 - 0x10ffff -> 11110www[3] 10xxxxxx[6] 10yyyyyy[6] 10zzzzzz[6]
|
|
||||||
|
|
||||||
UTF8_BOUNDARIES = [0x7f, 0x7ff, 0xffff, 0x10ffff]
|
|
||||||
|
|
||||||
def to_utf8_enc( n )
|
|
||||||
r = 0
|
|
||||||
if n <= 0x7f
|
|
||||||
r = n
|
|
||||||
elsif n <= 0x7ff
|
|
||||||
y = 0xc0 | (n >> 6)
|
|
||||||
z = 0x80 | (n & 0x3f)
|
|
||||||
r = y << 8 | z
|
|
||||||
elsif n <= 0xffff
|
|
||||||
x = 0xe0 | (n >> 12)
|
|
||||||
y = 0x80 | (n >> 6) & 0x3f
|
|
||||||
z = 0x80 | n & 0x3f
|
|
||||||
r = x << 16 | y << 8 | z
|
|
||||||
elsif n <= 0x10ffff
|
|
||||||
w = 0xf0 | (n >> 18)
|
|
||||||
x = 0x80 | (n >> 12) & 0x3f
|
|
||||||
y = 0x80 | (n >> 6) & 0x3f
|
|
||||||
z = 0x80 | n & 0x3f
|
|
||||||
r = w << 24 | x << 16 | y << 8 | z
|
|
||||||
end
|
|
||||||
|
|
||||||
to_hex(r)
|
|
||||||
end
|
|
||||||
|
|
||||||
def from_utf8_enc( n )
|
|
||||||
n = n.hex
|
|
||||||
r = 0
|
|
||||||
if n <= 0x7f
|
|
||||||
r = n
|
|
||||||
elsif n <= 0xdfff
|
|
||||||
y = (n >> 8) & 0x1f
|
|
||||||
z = n & 0x3f
|
|
||||||
r = y << 6 | z
|
|
||||||
elsif n <= 0xefffff
|
|
||||||
x = (n >> 16) & 0x0f
|
|
||||||
y = (n >> 8) & 0x3f
|
|
||||||
z = n & 0x3f
|
|
||||||
r = x << 10 | y << 6 | z
|
|
||||||
elsif n <= 0xf7ffffff
|
|
||||||
w = (n >> 24) & 0x07
|
|
||||||
x = (n >> 16) & 0x3f
|
|
||||||
y = (n >> 8) & 0x3f
|
|
||||||
z = n & 0x3f
|
|
||||||
r = w << 18 | x << 12 | y << 6 | z
|
|
||||||
end
|
|
||||||
r
|
|
||||||
end
|
|
||||||
|
|
||||||
###
|
|
||||||
# Given a range, splits it up into ranges that can be continuously
|
|
||||||
# encoded into utf8. Eg: 0x00 .. 0xff => [0x00..0x7f, 0x80..0xff]
|
|
||||||
# This is not strictly needed since the current [5.1] unicode standard
|
|
||||||
# doesn't have ranges that straddle utf8 boundaries. This is included
|
|
||||||
# for completeness as there is no telling if that will ever change.
|
|
||||||
|
|
||||||
def utf8_ranges( range )
|
|
||||||
ranges = []
|
|
||||||
UTF8_BOUNDARIES.each do |max|
|
|
||||||
if range.begin <= max
|
|
||||||
if range.end <= max
|
|
||||||
ranges << range
|
|
||||||
return ranges
|
|
||||||
end
|
|
||||||
|
|
||||||
ranges << (range.begin .. max)
|
|
||||||
range = (max + 1) .. range.end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ranges
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_range( start, stop )
|
|
||||||
size = start.size/2
|
|
||||||
left = size - 1
|
|
||||||
return [""] if size < 1
|
|
||||||
|
|
||||||
a = start[0..1]
|
|
||||||
b = stop[0..1]
|
|
||||||
|
|
||||||
###
|
|
||||||
# Shared prefix
|
|
||||||
|
|
||||||
if a == b
|
|
||||||
return build_range(start[2..-1], stop[2..-1]).map do |elt|
|
|
||||||
"0x#{a} " + elt
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
###
|
|
||||||
# Unshared prefix, end of run
|
|
||||||
|
|
||||||
return ["0x#{a}..0x#{b} "] if left.zero?
|
|
||||||
|
|
||||||
###
|
|
||||||
# Unshared prefix, not end of run
|
|
||||||
# Range can be 0x123456..0x56789A
|
|
||||||
# Which is equivalent to:
|
|
||||||
# 0x123456 .. 0x12FFFF
|
|
||||||
# 0x130000 .. 0x55FFFF
|
|
||||||
# 0x560000 .. 0x56789A
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
ret << build_range(start, a + "FF" * left)
|
|
||||||
|
|
||||||
###
|
|
||||||
# Only generate middle range if need be.
|
|
||||||
|
|
||||||
if a.hex+1 != b.hex
|
|
||||||
max = to_hex(b.hex - 1)
|
|
||||||
max = "FF" if b == "FF"
|
|
||||||
ret << "0x#{to_hex(a.hex+1)}..0x#{max} " + "0x00..0xFF " * left
|
|
||||||
end
|
|
||||||
|
|
||||||
###
|
|
||||||
# Don't generate last range if it is covered by first range
|
|
||||||
|
|
||||||
ret << build_range(b + "00" * left, stop) unless b == "FF"
|
|
||||||
ret.flatten!
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_utf8( range )
|
|
||||||
utf8_ranges( range ).map do |r|
|
|
||||||
begin_enc = to_utf8_enc(r.begin)
|
|
||||||
end_enc = to_utf8_enc(r.end)
|
|
||||||
build_range begin_enc, end_enc
|
|
||||||
end.flatten!
|
|
||||||
end
|
|
||||||
|
|
||||||
##
|
|
||||||
# Perform a 3-way comparison of the number of codepoints advertised by
|
|
||||||
# the unicode spec for the given range, the originally parsed range,
|
|
||||||
# and the resulting utf8 encoded range.
|
|
||||||
|
|
||||||
def count_codepoints( code )
|
|
||||||
code.split(' ').inject(1) do |acc, elt|
|
|
||||||
if elt =~ /0x(.+)\.\.0x(.+)/
|
|
||||||
if @encoding == :utf8
|
|
||||||
acc * (from_utf8_enc($2) - from_utf8_enc($1) + 1)
|
|
||||||
else
|
|
||||||
acc * ($2.hex - $1.hex + 1)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
acc
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def is_valid?( range, desc, codes )
|
|
||||||
spec_count = 1
|
|
||||||
spec_count = $1.to_i if desc =~ /\[(\d+)\]/
|
|
||||||
range_count = range.end - range.begin + 1
|
|
||||||
|
|
||||||
sum = codes.inject(0) { |acc, elt| acc + count_codepoints(elt) }
|
|
||||||
sum == spec_count and sum == range_count
|
|
||||||
end
|
|
||||||
|
|
||||||
##
|
|
||||||
# Generate the state maching to stdout
|
|
||||||
|
|
||||||
def generate_machine( name, property )
|
|
||||||
pipe = " "
|
|
||||||
@output.puts " #{name} = "
|
|
||||||
each_alpha( @chart_url, property ) do |range, desc|
|
|
||||||
|
|
||||||
codes = (@encoding == :ucs4) ? to_ucs4(range) : to_utf8(range)
|
|
||||||
|
|
||||||
#raise "Invalid encoding of range #{range}: #{codes.inspect}" unless
|
|
||||||
# is_valid? range, desc, codes
|
|
||||||
|
|
||||||
range_width = codes.map { |a| a.size }.max
|
|
||||||
range_width = RANGE_WIDTH if range_width < RANGE_WIDTH
|
|
||||||
|
|
||||||
desc_width = TOTAL_WIDTH - RANGE_WIDTH - 11
|
|
||||||
desc_width -= (range_width - RANGE_WIDTH) if range_width > RANGE_WIDTH
|
|
||||||
|
|
||||||
if desc.size > desc_width
|
|
||||||
desc = desc[0..desc_width - 4] + "..."
|
|
||||||
end
|
|
||||||
|
|
||||||
codes.each_with_index do |r, idx|
|
|
||||||
desc = "" unless idx.zero?
|
|
||||||
code = "%-#{range_width}s" % r
|
|
||||||
@output.puts " #{pipe} #{code} ##{desc}"
|
|
||||||
pipe = "|"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@output.puts " ;"
|
|
||||||
@output.puts ""
|
|
||||||
end
|
|
||||||
|
|
||||||
@output.puts <<EOF
|
|
||||||
# The following Ragel file was autogenerated with #{$0}
|
|
||||||
# from: #{@chart_url}
|
|
||||||
#
|
|
||||||
# It defines #{properties}.
|
|
||||||
#
|
|
||||||
# To use this, make sure that your alphtype is set to #{ALPHTYPES[@encoding]},
|
|
||||||
# and that your input is in #{@encoding}.
|
|
||||||
|
|
||||||
%%{
|
|
||||||
machine #{machine_name};
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
properties.each { |x| generate_machine( x, x ) }
|
|
||||||
|
|
||||||
@output.puts <<EOF
|
|
||||||
}%%
|
|
||||||
EOF
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,86 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Variables returns all of the variables referenced within a given experssion.
|
|
||||||
//
|
|
||||||
// This is the implementation of the "Variables" method on every native
|
|
||||||
// expression.
|
|
||||||
func Variables(expr Expression) []hcl.Traversal {
|
|
||||||
var vars []hcl.Traversal
|
|
||||||
|
|
||||||
walker := &variablesWalker{
|
|
||||||
Callback: func(t hcl.Traversal) {
|
|
||||||
vars = append(vars, t)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
Walk(expr, walker)
|
|
||||||
|
|
||||||
return vars
|
|
||||||
}
|
|
||||||
|
|
||||||
// variablesWalker is a Walker implementation that calls its callback for any
|
|
||||||
// root scope traversal found while walking.
|
|
||||||
type variablesWalker struct {
|
|
||||||
Callback func(hcl.Traversal)
|
|
||||||
localScopes []map[string]struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *variablesWalker) Enter(n Node) hcl.Diagnostics {
|
|
||||||
switch tn := n.(type) {
|
|
||||||
case *ScopeTraversalExpr:
|
|
||||||
t := tn.Traversal
|
|
||||||
|
|
||||||
// Check if the given root name appears in any of the active
|
|
||||||
// local scopes. We don't want to return local variables here, since
|
|
||||||
// the goal of walking variables is to tell the calling application
|
|
||||||
// which names it needs to populate in the _root_ scope.
|
|
||||||
name := t.RootName()
|
|
||||||
for _, names := range w.localScopes {
|
|
||||||
if _, localized := names[name]; localized {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Callback(t)
|
|
||||||
case ChildScope:
|
|
||||||
w.localScopes = append(w.localScopes, tn.LocalNames)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *variablesWalker) Exit(n Node) hcl.Diagnostics {
|
|
||||||
switch n.(type) {
|
|
||||||
case ChildScope:
|
|
||||||
// pop the latest local scope, assuming that the walker will
|
|
||||||
// behave symmetrically as promised.
|
|
||||||
w.localScopes = w.localScopes[:len(w.localScopes)-1]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChildScope is a synthetic AST node that is visited during a walk to
|
|
||||||
// indicate that its descendent will be evaluated in a child scope, which
|
|
||||||
// may mask certain variables from the parent scope as locals.
|
|
||||||
//
|
|
||||||
// ChildScope nodes don't really exist in the AST, but are rather synthesized
|
|
||||||
// on the fly during walk. Therefore it doesn't do any good to transform them;
|
|
||||||
// instead, transform either parent node that created a scope or the expression
|
|
||||||
// that the child scope struct wraps.
|
|
||||||
type ChildScope struct {
|
|
||||||
LocalNames map[string]struct{}
|
|
||||||
Expr Expression
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ChildScope) walkChildNodes(w internalWalkFunc) {
|
|
||||||
w(e.Expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Range returns the range of the expression that the ChildScope is
|
|
||||||
// encapsulating. It isn't really very useful to call Range on a ChildScope.
|
|
||||||
func (e ChildScope) Range() hcl.Range {
|
|
||||||
return e.Expr.Range()
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package hclsyntax
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// VisitFunc is the callback signature for VisitAll.
|
|
||||||
type VisitFunc func(node Node) hcl.Diagnostics
|
|
||||||
|
|
||||||
// VisitAll is a basic way to traverse the AST beginning with a particular
|
|
||||||
// node. The given function will be called once for each AST node in
|
|
||||||
// depth-first order, but no context is provided about the shape of the tree.
|
|
||||||
//
|
|
||||||
// The VisitFunc may return diagnostics, in which case they will be accumulated
|
|
||||||
// and returned as a single set.
|
|
||||||
func VisitAll(node Node, f VisitFunc) hcl.Diagnostics {
|
|
||||||
diags := f(node)
|
|
||||||
node.walkChildNodes(func(node Node) {
|
|
||||||
diags = append(diags, VisitAll(node, f)...)
|
|
||||||
})
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walker is an interface used with Walk.
|
|
||||||
type Walker interface {
|
|
||||||
Enter(node Node) hcl.Diagnostics
|
|
||||||
Exit(node Node) hcl.Diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk is a more complex way to traverse the AST starting with a particular
|
|
||||||
// node, which provides information about the tree structure via separate
|
|
||||||
// Enter and Exit functions.
|
|
||||||
func Walk(node Node, w Walker) hcl.Diagnostics {
|
|
||||||
diags := w.Enter(node)
|
|
||||||
node.walkChildNodes(func(node Node) {
|
|
||||||
diags = append(diags, Walk(node, w)...)
|
|
||||||
})
|
|
||||||
moreDiags := w.Exit(node)
|
|
||||||
diags = append(diags, moreDiags...)
|
|
||||||
return diags
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
type node interface {
|
|
||||||
Range() hcl.Range
|
|
||||||
StartRange() hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
type objectVal struct {
|
|
||||||
Attrs []*objectAttr
|
|
||||||
SrcRange hcl.Range // range of the entire object, brace-to-brace
|
|
||||||
OpenRange hcl.Range // range of the opening brace
|
|
||||||
CloseRange hcl.Range // range of the closing brace
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *objectVal) Range() hcl.Range {
|
|
||||||
return n.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *objectVal) StartRange() hcl.Range {
|
|
||||||
return n.OpenRange
|
|
||||||
}
|
|
||||||
|
|
||||||
type objectAttr struct {
|
|
||||||
Name string
|
|
||||||
Value node
|
|
||||||
NameRange hcl.Range // range of the name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *objectAttr) Range() hcl.Range {
|
|
||||||
return n.NameRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *objectAttr) StartRange() hcl.Range {
|
|
||||||
return n.NameRange
|
|
||||||
}
|
|
||||||
|
|
||||||
type arrayVal struct {
|
|
||||||
Values []node
|
|
||||||
SrcRange hcl.Range // range of the entire object, bracket-to-bracket
|
|
||||||
OpenRange hcl.Range // range of the opening bracket
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *arrayVal) Range() hcl.Range {
|
|
||||||
return n.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *arrayVal) StartRange() hcl.Range {
|
|
||||||
return n.OpenRange
|
|
||||||
}
|
|
||||||
|
|
||||||
type booleanVal struct {
|
|
||||||
Value bool
|
|
||||||
SrcRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *booleanVal) Range() hcl.Range {
|
|
||||||
return n.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *booleanVal) StartRange() hcl.Range {
|
|
||||||
return n.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
type numberVal struct {
|
|
||||||
Value *big.Float
|
|
||||||
SrcRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *numberVal) Range() hcl.Range {
|
|
||||||
return n.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *numberVal) StartRange() hcl.Range {
|
|
||||||
return n.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
type stringVal struct {
|
|
||||||
Value string
|
|
||||||
SrcRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *stringVal) Range() hcl.Range {
|
|
||||||
return n.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *stringVal) StartRange() hcl.Range {
|
|
||||||
return n.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
type nullVal struct {
|
|
||||||
SrcRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nullVal) Range() hcl.Range {
|
|
||||||
return n.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *nullVal) StartRange() hcl.Range {
|
|
||||||
return n.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
// invalidVal is used as a placeholder where a value is needed for a valid
|
|
||||||
// parse tree but the input was invalid enough to prevent one from being
|
|
||||||
// created.
|
|
||||||
type invalidVal struct {
|
|
||||||
SrcRange hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n invalidVal) Range() hcl.Range {
|
|
||||||
return n.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n invalidVal) StartRange() hcl.Range {
|
|
||||||
return n.SrcRange
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/agext/levenshtein"
|
|
||||||
)
|
|
||||||
|
|
||||||
var keywords = []string{"false", "true", "null"}
|
|
||||||
|
|
||||||
// keywordSuggestion tries to find a valid JSON keyword that is close to the
|
|
||||||
// given string and returns it if found. If no keyword is close enough, returns
|
|
||||||
// the empty string.
|
|
||||||
func keywordSuggestion(given string) string {
|
|
||||||
return nameSuggestion(given, keywords)
|
|
||||||
}
|
|
||||||
|
|
||||||
// nameSuggestion tries to find a name from the given slice of suggested names
|
|
||||||
// that is close to the given name and returns it if found. If no suggestion
|
|
||||||
// is close enough, returns the empty string.
|
|
||||||
//
|
|
||||||
// The suggestions are tried in order, so earlier suggestions take precedence
|
|
||||||
// if the given string is similar to two or more suggestions.
|
|
||||||
//
|
|
||||||
// This function is intended to be used with a relatively-small number of
|
|
||||||
// suggestions. It's not optimized for hundreds or thousands of them.
|
|
||||||
func nameSuggestion(given string, suggestions []string) string {
|
|
||||||
for _, suggestion := range suggestions {
|
|
||||||
dist := levenshtein.Distance(given, suggestion, nil)
|
|
||||||
if dist < 3 { // threshold determined experimentally
|
|
||||||
return suggestion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
// Package json is the JSON parser for HCL. It parses JSON files and returns
|
|
||||||
// implementations of the core HCL structural interfaces in terms of the
|
|
||||||
// JSON data inside.
|
|
||||||
//
|
|
||||||
// This is not a generic JSON parser. Instead, it deals with the mapping from
|
|
||||||
// the JSON information model to the HCL information model, using a number
|
|
||||||
// of hard-coded structural conventions.
|
|
||||||
package json
|
|
|
@ -1,70 +0,0 @@
|
||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type navigation struct {
|
|
||||||
root node
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation of hcled.ContextString
|
|
||||||
func (n navigation) ContextString(offset int) string {
|
|
||||||
steps := navigationStepsRev(n.root, offset)
|
|
||||||
if steps == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// We built our slice backwards, so we'll reverse it in-place now.
|
|
||||||
half := len(steps) / 2 // integer division
|
|
||||||
for i := 0; i < half; i++ {
|
|
||||||
steps[i], steps[len(steps)-1-i] = steps[len(steps)-1-i], steps[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
ret := strings.Join(steps, "")
|
|
||||||
if len(ret) > 0 && ret[0] == '.' {
|
|
||||||
ret = ret[1:]
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigationStepsRev(v node, offset int) []string {
|
|
||||||
switch tv := v.(type) {
|
|
||||||
case *objectVal:
|
|
||||||
// Do any of our properties have an object that contains the target
|
|
||||||
// offset?
|
|
||||||
for _, attr := range tv.Attrs {
|
|
||||||
k := attr.Name
|
|
||||||
av := attr.Value
|
|
||||||
|
|
||||||
switch av.(type) {
|
|
||||||
case *objectVal, *arrayVal:
|
|
||||||
// okay
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if av.Range().ContainsOffset(offset) {
|
|
||||||
return append(navigationStepsRev(av, offset), "."+k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case *arrayVal:
|
|
||||||
// Do any of our elements contain the target offset?
|
|
||||||
for i, elem := range tv.Values {
|
|
||||||
|
|
||||||
switch elem.(type) {
|
|
||||||
case *objectVal, *arrayVal:
|
|
||||||
// okay
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if elem.Range().ContainsOffset(offset) {
|
|
||||||
return append(navigationStepsRev(elem, offset), fmt.Sprintf("[%d]", i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,496 +0,0 @@
|
||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
|
||||||
|
|
||||||
func parseFileContent(buf []byte, filename string) (node, hcl.Diagnostics) {
|
|
||||||
tokens := scan(buf, pos{
|
|
||||||
Filename: filename,
|
|
||||||
Pos: hcl.Pos{
|
|
||||||
Byte: 0,
|
|
||||||
Line: 1,
|
|
||||||
Column: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
p := newPeeker(tokens)
|
|
||||||
node, diags := parseValue(p)
|
|
||||||
if len(diags) == 0 && p.Peek().Type != tokenEOF {
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Extraneous data after value",
|
|
||||||
Detail: "Extra characters appear after the JSON value.",
|
|
||||||
Subject: p.Peek().Range.Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return node, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseValue(p *peeker) (node, hcl.Diagnostics) {
|
|
||||||
tok := p.Peek()
|
|
||||||
|
|
||||||
wrapInvalid := func(n node, diags hcl.Diagnostics) (node, hcl.Diagnostics) {
|
|
||||||
if n != nil {
|
|
||||||
return n, diags
|
|
||||||
}
|
|
||||||
return invalidVal{tok.Range}, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
switch tok.Type {
|
|
||||||
case tokenBraceO:
|
|
||||||
return wrapInvalid(parseObject(p))
|
|
||||||
case tokenBrackO:
|
|
||||||
return wrapInvalid(parseArray(p))
|
|
||||||
case tokenNumber:
|
|
||||||
return wrapInvalid(parseNumber(p))
|
|
||||||
case tokenString:
|
|
||||||
return wrapInvalid(parseString(p))
|
|
||||||
case tokenKeyword:
|
|
||||||
return wrapInvalid(parseKeyword(p))
|
|
||||||
case tokenBraceC:
|
|
||||||
return wrapInvalid(nil, hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Missing JSON value",
|
|
||||||
Detail: "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.",
|
|
||||||
Subject: &tok.Range,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
case tokenBrackC:
|
|
||||||
return wrapInvalid(nil, hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Missing array element value",
|
|
||||||
Detail: "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.",
|
|
||||||
Subject: &tok.Range,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
case tokenEOF:
|
|
||||||
return wrapInvalid(nil, hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Missing value",
|
|
||||||
Detail: "The JSON data ends prematurely.",
|
|
||||||
Subject: &tok.Range,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return wrapInvalid(nil, hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid start of value",
|
|
||||||
Detail: "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.",
|
|
||||||
Subject: &tok.Range,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func tokenCanStartValue(tok token) bool {
|
|
||||||
switch tok.Type {
|
|
||||||
case tokenBraceO, tokenBrackO, tokenNumber, tokenString, tokenKeyword:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseObject(p *peeker) (node, hcl.Diagnostics) {
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
|
|
||||||
open := p.Read()
|
|
||||||
attrs := []*objectAttr{}
|
|
||||||
|
|
||||||
// recover is used to shift the peeker to what seems to be the end of
|
|
||||||
// our object, so that when we encounter an error we leave the peeker
|
|
||||||
// at a reasonable point in the token stream to continue parsing.
|
|
||||||
recover := func(tok token) {
|
|
||||||
open := 1
|
|
||||||
for {
|
|
||||||
switch tok.Type {
|
|
||||||
case tokenBraceO:
|
|
||||||
open++
|
|
||||||
case tokenBraceC:
|
|
||||||
open--
|
|
||||||
if open <= 1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case tokenEOF:
|
|
||||||
// Ran out of source before we were able to recover,
|
|
||||||
// so we'll bail here and let the caller deal with it.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tok = p.Read()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Token:
|
|
||||||
for {
|
|
||||||
if p.Peek().Type == tokenBraceC {
|
|
||||||
break Token
|
|
||||||
}
|
|
||||||
|
|
||||||
keyNode, keyDiags := parseValue(p)
|
|
||||||
diags = diags.Extend(keyDiags)
|
|
||||||
if keyNode == nil {
|
|
||||||
return nil, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
keyStrNode, ok := keyNode.(*stringVal)
|
|
||||||
if !ok {
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid object property name",
|
|
||||||
Detail: "A JSON object property name must be a string",
|
|
||||||
Subject: keyNode.StartRange().Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
key := keyStrNode.Value
|
|
||||||
|
|
||||||
colon := p.Read()
|
|
||||||
if colon.Type != tokenColon {
|
|
||||||
recover(colon)
|
|
||||||
|
|
||||||
if colon.Type == tokenBraceC || colon.Type == tokenComma {
|
|
||||||
// Catch common mistake of using braces instead of brackets
|
|
||||||
// for an object.
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Missing object value",
|
|
||||||
Detail: "A JSON object attribute must have a value, introduced by a colon.",
|
|
||||||
Subject: &colon.Range,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if colon.Type == tokenEquals {
|
|
||||||
// Possible confusion with native HCL syntax.
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Missing property value colon",
|
|
||||||
Detail: "JSON uses a colon as its name/value delimiter, not an equals sign.",
|
|
||||||
Subject: &colon.Range,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Missing property value colon",
|
|
||||||
Detail: "A colon must appear between an object property's name and its value.",
|
|
||||||
Subject: &colon.Range,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
valNode, valDiags := parseValue(p)
|
|
||||||
diags = diags.Extend(valDiags)
|
|
||||||
if valNode == nil {
|
|
||||||
return nil, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs = append(attrs, &objectAttr{
|
|
||||||
Name: key,
|
|
||||||
Value: valNode,
|
|
||||||
NameRange: keyStrNode.SrcRange,
|
|
||||||
})
|
|
||||||
|
|
||||||
switch p.Peek().Type {
|
|
||||||
case tokenComma:
|
|
||||||
comma := p.Read()
|
|
||||||
if p.Peek().Type == tokenBraceC {
|
|
||||||
// Special error message for this common mistake
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Trailing comma in object",
|
|
||||||
Detail: "JSON does not permit a trailing comma after the final property in an object.",
|
|
||||||
Subject: &comma.Range,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
continue Token
|
|
||||||
case tokenEOF:
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unclosed object",
|
|
||||||
Detail: "No closing brace was found for this JSON object.",
|
|
||||||
Subject: &open.Range,
|
|
||||||
})
|
|
||||||
case tokenBrackC:
|
|
||||||
// Consume the bracket anyway, so that we don't return with the peeker
|
|
||||||
// at a strange place.
|
|
||||||
p.Read()
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Mismatched braces",
|
|
||||||
Detail: "A JSON object must be closed with a brace, not a bracket.",
|
|
||||||
Subject: p.Peek().Range.Ptr(),
|
|
||||||
})
|
|
||||||
case tokenBraceC:
|
|
||||||
break Token
|
|
||||||
default:
|
|
||||||
recover(p.Read())
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Missing attribute seperator comma",
|
|
||||||
Detail: "A comma must appear between each property definition in an object.",
|
|
||||||
Subject: p.Peek().Range.Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
close := p.Read()
|
|
||||||
return &objectVal{
|
|
||||||
Attrs: attrs,
|
|
||||||
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
||||||
OpenRange: open.Range,
|
|
||||||
CloseRange: close.Range,
|
|
||||||
}, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseArray(p *peeker) (node, hcl.Diagnostics) {
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
|
|
||||||
open := p.Read()
|
|
||||||
vals := []node{}
|
|
||||||
|
|
||||||
// recover is used to shift the peeker to what seems to be the end of
|
|
||||||
// our array, so that when we encounter an error we leave the peeker
|
|
||||||
// at a reasonable point in the token stream to continue parsing.
|
|
||||||
recover := func(tok token) {
|
|
||||||
open := 1
|
|
||||||
for {
|
|
||||||
switch tok.Type {
|
|
||||||
case tokenBrackO:
|
|
||||||
open++
|
|
||||||
case tokenBrackC:
|
|
||||||
open--
|
|
||||||
if open <= 1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case tokenEOF:
|
|
||||||
// Ran out of source before we were able to recover,
|
|
||||||
// so we'll bail here and let the caller deal with it.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tok = p.Read()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Token:
|
|
||||||
for {
|
|
||||||
if p.Peek().Type == tokenBrackC {
|
|
||||||
break Token
|
|
||||||
}
|
|
||||||
|
|
||||||
valNode, valDiags := parseValue(p)
|
|
||||||
diags = diags.Extend(valDiags)
|
|
||||||
if valNode == nil {
|
|
||||||
return nil, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
vals = append(vals, valNode)
|
|
||||||
|
|
||||||
switch p.Peek().Type {
|
|
||||||
case tokenComma:
|
|
||||||
comma := p.Read()
|
|
||||||
if p.Peek().Type == tokenBrackC {
|
|
||||||
// Special error message for this common mistake
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Trailing comma in array",
|
|
||||||
Detail: "JSON does not permit a trailing comma after the final value in an array.",
|
|
||||||
Subject: &comma.Range,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
continue Token
|
|
||||||
case tokenColon:
|
|
||||||
recover(p.Read())
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid array value",
|
|
||||||
Detail: "A colon is not used to introduce values in a JSON array.",
|
|
||||||
Subject: p.Peek().Range.Ptr(),
|
|
||||||
})
|
|
||||||
case tokenEOF:
|
|
||||||
recover(p.Read())
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Unclosed object",
|
|
||||||
Detail: "No closing bracket was found for this JSON array.",
|
|
||||||
Subject: &open.Range,
|
|
||||||
})
|
|
||||||
case tokenBraceC:
|
|
||||||
recover(p.Read())
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Mismatched brackets",
|
|
||||||
Detail: "A JSON array must be closed with a bracket, not a brace.",
|
|
||||||
Subject: p.Peek().Range.Ptr(),
|
|
||||||
})
|
|
||||||
case tokenBrackC:
|
|
||||||
break Token
|
|
||||||
default:
|
|
||||||
recover(p.Read())
|
|
||||||
return nil, diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Missing attribute seperator comma",
|
|
||||||
Detail: "A comma must appear between each value in an array.",
|
|
||||||
Subject: p.Peek().Range.Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
close := p.Read()
|
|
||||||
return &arrayVal{
|
|
||||||
Values: vals,
|
|
||||||
SrcRange: hcl.RangeBetween(open.Range, close.Range),
|
|
||||||
OpenRange: open.Range,
|
|
||||||
}, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseNumber(p *peeker) (node, hcl.Diagnostics) {
|
|
||||||
tok := p.Read()
|
|
||||||
|
|
||||||
// Use encoding/json to validate the number syntax.
|
|
||||||
// TODO: Do this more directly to produce better diagnostics.
|
|
||||||
var num json.Number
|
|
||||||
err := json.Unmarshal(tok.Bytes, &num)
|
|
||||||
if err != nil {
|
|
||||||
return nil, hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid JSON number",
|
|
||||||
Detail: fmt.Sprintf("There is a syntax error in the given JSON number."),
|
|
||||||
Subject: &tok.Range,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to guarantee that we parse numbers the same way as cty (and thus
|
|
||||||
// native syntax HCL) would here, so we'll use the cty parser even though
|
|
||||||
// in most other cases we don't actually introduce cty concepts until
|
|
||||||
// decoding time. We'll unwrap the parsed float immediately afterwards, so
|
|
||||||
// the cty value is just a temporary helper.
|
|
||||||
nv, err := cty.ParseNumberVal(string(num))
|
|
||||||
if err != nil {
|
|
||||||
// Should never happen if above passed, since JSON numbers are a subset
|
|
||||||
// of what cty can parse...
|
|
||||||
return nil, hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid JSON number",
|
|
||||||
Detail: fmt.Sprintf("There is a syntax error in the given JSON number."),
|
|
||||||
Subject: &tok.Range,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &numberVal{
|
|
||||||
Value: nv.AsBigFloat(),
|
|
||||||
SrcRange: tok.Range,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseString(p *peeker) (node, hcl.Diagnostics) {
|
|
||||||
tok := p.Read()
|
|
||||||
var str string
|
|
||||||
err := json.Unmarshal(tok.Bytes, &str)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
var errRange hcl.Range
|
|
||||||
if serr, ok := err.(*json.SyntaxError); ok {
|
|
||||||
errOfs := serr.Offset
|
|
||||||
errPos := tok.Range.Start
|
|
||||||
errPos.Byte += int(errOfs)
|
|
||||||
|
|
||||||
// TODO: Use the byte offset to properly count unicode
|
|
||||||
// characters for the column, and mark the whole of the
|
|
||||||
// character that was wrong as part of our range.
|
|
||||||
errPos.Column += int(errOfs)
|
|
||||||
|
|
||||||
errEndPos := errPos
|
|
||||||
errEndPos.Byte++
|
|
||||||
errEndPos.Column++
|
|
||||||
|
|
||||||
errRange = hcl.Range{
|
|
||||||
Filename: tok.Range.Filename,
|
|
||||||
Start: errPos,
|
|
||||||
End: errEndPos,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errRange = tok.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
var contextRange *hcl.Range
|
|
||||||
if errRange != tok.Range {
|
|
||||||
contextRange = &tok.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Eventually we should parse strings directly here so
|
|
||||||
// we can produce a more useful error message in the face fo things
|
|
||||||
// such as invalid escapes, etc.
|
|
||||||
return nil, hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid JSON string",
|
|
||||||
Detail: fmt.Sprintf("There is a syntax error in the given JSON string."),
|
|
||||||
Subject: &errRange,
|
|
||||||
Context: contextRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &stringVal{
|
|
||||||
Value: str,
|
|
||||||
SrcRange: tok.Range,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseKeyword(p *peeker) (node, hcl.Diagnostics) {
|
|
||||||
tok := p.Read()
|
|
||||||
s := string(tok.Bytes)
|
|
||||||
|
|
||||||
switch s {
|
|
||||||
case "true":
|
|
||||||
return &booleanVal{
|
|
||||||
Value: true,
|
|
||||||
SrcRange: tok.Range,
|
|
||||||
}, nil
|
|
||||||
case "false":
|
|
||||||
return &booleanVal{
|
|
||||||
Value: false,
|
|
||||||
SrcRange: tok.Range,
|
|
||||||
}, nil
|
|
||||||
case "null":
|
|
||||||
return &nullVal{
|
|
||||||
SrcRange: tok.Range,
|
|
||||||
}, nil
|
|
||||||
case "undefined", "NaN", "Infinity":
|
|
||||||
return nil, hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid JSON keyword",
|
|
||||||
Detail: fmt.Sprintf("The JavaScript identifier %q cannot be used in JSON.", s),
|
|
||||||
Subject: &tok.Range,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
var dym string
|
|
||||||
if suggest := keywordSuggestion(s); suggest != "" {
|
|
||||||
dym = fmt.Sprintf(" Did you mean %q?", suggest)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid JSON keyword",
|
|
||||||
Detail: fmt.Sprintf("%q is not a valid JSON keyword.%s", s, dym),
|
|
||||||
Subject: &tok.Range,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
package json
|
|
||||||
|
|
||||||
type peeker struct {
|
|
||||||
tokens []token
|
|
||||||
pos int
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPeeker(tokens []token) *peeker {
|
|
||||||
return &peeker{
|
|
||||||
tokens: tokens,
|
|
||||||
pos: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peeker) Peek() token {
|
|
||||||
return p.tokens[p.pos]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *peeker) Read() token {
|
|
||||||
ret := p.tokens[p.pos]
|
|
||||||
if ret.Type != tokenEOF {
|
|
||||||
p.pos++
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parse attempts to parse the given buffer as JSON and, if successful, returns
|
|
||||||
// a hcl.File for the HCL configuration represented by it.
|
|
||||||
//
|
|
||||||
// This is not a generic JSON parser. Instead, it deals only with the profile
|
|
||||||
// of JSON used to express HCL configuration.
|
|
||||||
//
|
|
||||||
// The returned file is valid only if the returned diagnostics returns false
|
|
||||||
// from its HasErrors method. If HasErrors returns true, the file represents
|
|
||||||
// the subset of data that was able to be parsed, which may be none.
|
|
||||||
func Parse(src []byte, filename string) (*hcl.File, hcl.Diagnostics) {
|
|
||||||
rootNode, diags := parseFileContent(src, filename)
|
|
||||||
|
|
||||||
switch rootNode.(type) {
|
|
||||||
case *objectVal, *arrayVal:
|
|
||||||
// okay
|
|
||||||
default:
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Root value must be object",
|
|
||||||
Detail: "The root value in a JSON-based configuration must be either a JSON object or a JSON array of objects.",
|
|
||||||
Subject: rootNode.StartRange().Ptr(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Since we've already produced an error message for this being
|
|
||||||
// invalid, we'll return an empty placeholder here so that trying to
|
|
||||||
// extract content from our root body won't produce a redundant
|
|
||||||
// error saying the same thing again in more general terms.
|
|
||||||
fakePos := hcl.Pos{
|
|
||||||
Byte: 0,
|
|
||||||
Line: 1,
|
|
||||||
Column: 1,
|
|
||||||
}
|
|
||||||
fakeRange := hcl.Range{
|
|
||||||
Filename: filename,
|
|
||||||
Start: fakePos,
|
|
||||||
End: fakePos,
|
|
||||||
}
|
|
||||||
rootNode = &objectVal{
|
|
||||||
Attrs: []*objectAttr{},
|
|
||||||
SrcRange: fakeRange,
|
|
||||||
OpenRange: fakeRange,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file := &hcl.File{
|
|
||||||
Body: &body{
|
|
||||||
val: rootNode,
|
|
||||||
},
|
|
||||||
Bytes: src,
|
|
||||||
Nav: navigation{rootNode},
|
|
||||||
}
|
|
||||||
return file, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseFile is a convenience wrapper around Parse that first attempts to load
|
|
||||||
// data from the given filename, passing the result to Parse if successful.
|
|
||||||
//
|
|
||||||
// If the file cannot be read, an error diagnostic with nil context is returned.
|
|
||||||
func ParseFile(filename string) (*hcl.File, hcl.Diagnostics) {
|
|
||||||
f, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Failed to open file",
|
|
||||||
Detail: fmt.Sprintf("The file %q could not be opened.", filename),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
src, err := ioutil.ReadAll(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Failed to read file",
|
|
||||||
Detail: fmt.Sprintf("The file %q was opened, but an error occured while reading it.", filename),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Parse(src, filename)
|
|
||||||
}
|
|
|
@ -1,297 +0,0 @@
|
||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/apparentlymart/go-textseg/textseg"
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:generate stringer -type tokenType scanner.go
|
|
||||||
type tokenType rune
|
|
||||||
|
|
||||||
const (
|
|
||||||
tokenBraceO tokenType = '{'
|
|
||||||
tokenBraceC tokenType = '}'
|
|
||||||
tokenBrackO tokenType = '['
|
|
||||||
tokenBrackC tokenType = ']'
|
|
||||||
tokenComma tokenType = ','
|
|
||||||
tokenColon tokenType = ':'
|
|
||||||
tokenKeyword tokenType = 'K'
|
|
||||||
tokenString tokenType = 'S'
|
|
||||||
tokenNumber tokenType = 'N'
|
|
||||||
tokenEOF tokenType = '␄'
|
|
||||||
tokenInvalid tokenType = 0
|
|
||||||
tokenEquals tokenType = '=' // used only for reminding the user of JSON syntax
|
|
||||||
)
|
|
||||||
|
|
||||||
type token struct {
|
|
||||||
Type tokenType
|
|
||||||
Bytes []byte
|
|
||||||
Range hcl.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
// scan returns the primary tokens for the given JSON buffer in sequence.
|
|
||||||
//
|
|
||||||
// The responsibility of this pass is to just mark the slices of the buffer
|
|
||||||
// as being of various types. It is lax in how it interprets the multi-byte
|
|
||||||
// token types keyword, string and number, preferring to capture erroneous
|
|
||||||
// extra bytes that we presume the user intended to be part of the token
|
|
||||||
// so that we can generate more helpful diagnostics in the parser.
|
|
||||||
func scan(buf []byte, start pos) []token {
|
|
||||||
var tokens []token
|
|
||||||
p := start
|
|
||||||
for {
|
|
||||||
if len(buf) == 0 {
|
|
||||||
tokens = append(tokens, token{
|
|
||||||
Type: tokenEOF,
|
|
||||||
Bytes: nil,
|
|
||||||
Range: posRange(p, p),
|
|
||||||
})
|
|
||||||
return tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, p = skipWhitespace(buf, p)
|
|
||||||
|
|
||||||
if len(buf) == 0 {
|
|
||||||
tokens = append(tokens, token{
|
|
||||||
Type: tokenEOF,
|
|
||||||
Bytes: nil,
|
|
||||||
Range: posRange(p, p),
|
|
||||||
})
|
|
||||||
return tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
start = p
|
|
||||||
|
|
||||||
first := buf[0]
|
|
||||||
switch {
|
|
||||||
case first == '{' || first == '}' || first == '[' || first == ']' || first == ',' || first == ':' || first == '=':
|
|
||||||
p.Pos.Column++
|
|
||||||
p.Pos.Byte++
|
|
||||||
tokens = append(tokens, token{
|
|
||||||
Type: tokenType(first),
|
|
||||||
Bytes: buf[0:1],
|
|
||||||
Range: posRange(start, p),
|
|
||||||
})
|
|
||||||
buf = buf[1:]
|
|
||||||
case first == '"':
|
|
||||||
var tokBuf []byte
|
|
||||||
tokBuf, buf, p = scanString(buf, p)
|
|
||||||
tokens = append(tokens, token{
|
|
||||||
Type: tokenString,
|
|
||||||
Bytes: tokBuf,
|
|
||||||
Range: posRange(start, p),
|
|
||||||
})
|
|
||||||
case byteCanStartNumber(first):
|
|
||||||
var tokBuf []byte
|
|
||||||
tokBuf, buf, p = scanNumber(buf, p)
|
|
||||||
tokens = append(tokens, token{
|
|
||||||
Type: tokenNumber,
|
|
||||||
Bytes: tokBuf,
|
|
||||||
Range: posRange(start, p),
|
|
||||||
})
|
|
||||||
case byteCanStartKeyword(first):
|
|
||||||
var tokBuf []byte
|
|
||||||
tokBuf, buf, p = scanKeyword(buf, p)
|
|
||||||
tokens = append(tokens, token{
|
|
||||||
Type: tokenKeyword,
|
|
||||||
Bytes: tokBuf,
|
|
||||||
Range: posRange(start, p),
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
tokens = append(tokens, token{
|
|
||||||
Type: tokenInvalid,
|
|
||||||
Bytes: buf[:1],
|
|
||||||
Range: start.Range(1, 1),
|
|
||||||
})
|
|
||||||
// If we've encountered an invalid then we might as well stop
|
|
||||||
// scanning since the parser won't proceed beyond this point.
|
|
||||||
return tokens
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func byteCanStartNumber(b byte) bool {
|
|
||||||
switch b {
|
|
||||||
// We are slightly more tolerant than JSON requires here since we
|
|
||||||
// expect the parser will make a stricter interpretation of the
|
|
||||||
// number bytes, but we specifically don't allow 'e' or 'E' here
|
|
||||||
// since we want the scanner to treat that as the start of an
|
|
||||||
// invalid keyword instead, to produce more intelligible error messages.
|
|
||||||
case '-', '+', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanNumber(buf []byte, start pos) ([]byte, []byte, pos) {
|
|
||||||
// The scanner doesn't check that the sequence of digit-ish bytes is
|
|
||||||
// in a valid order. The parser must do this when decoding a number
|
|
||||||
// token.
|
|
||||||
var i int
|
|
||||||
p := start
|
|
||||||
Byte:
|
|
||||||
for i = 0; i < len(buf); i++ {
|
|
||||||
switch buf[i] {
|
|
||||||
case '-', '+', '.', 'e', 'E', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
|
||||||
p.Pos.Byte++
|
|
||||||
p.Pos.Column++
|
|
||||||
default:
|
|
||||||
break Byte
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf[:i], buf[i:], p
|
|
||||||
}
|
|
||||||
|
|
||||||
func byteCanStartKeyword(b byte) bool {
|
|
||||||
switch {
|
|
||||||
// We allow any sequence of alphabetical characters here, even though
|
|
||||||
// JSON is more constrained, so that we can collect what we presume
|
|
||||||
// the user intended to be a single keyword and then check its validity
|
|
||||||
// in the parser, where we can generate better diagnostics.
|
|
||||||
// So e.g. we want to be able to say:
|
|
||||||
// unrecognized keyword "True". Did you mean "true"?
|
|
||||||
case isAlphabetical(b):
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanKeyword(buf []byte, start pos) ([]byte, []byte, pos) {
|
|
||||||
var i int
|
|
||||||
p := start
|
|
||||||
Byte:
|
|
||||||
for i = 0; i < len(buf); i++ {
|
|
||||||
b := buf[i]
|
|
||||||
switch {
|
|
||||||
case isAlphabetical(b) || b == '_':
|
|
||||||
p.Pos.Byte++
|
|
||||||
p.Pos.Column++
|
|
||||||
default:
|
|
||||||
break Byte
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf[:i], buf[i:], p
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanString(buf []byte, start pos) ([]byte, []byte, pos) {
|
|
||||||
// The scanner doesn't validate correct use of escapes, etc. It pays
|
|
||||||
// attention to escapes only for the purpose of identifying the closing
|
|
||||||
// quote character. It's the parser's responsibility to do proper
|
|
||||||
// validation.
|
|
||||||
//
|
|
||||||
// The scanner also doesn't specifically detect unterminated string
|
|
||||||
// literals, though they can be identified in the parser by checking if
|
|
||||||
// the final byte in a string token is the double-quote character.
|
|
||||||
|
|
||||||
// Skip the opening quote symbol
|
|
||||||
i := 1
|
|
||||||
p := start
|
|
||||||
p.Pos.Byte++
|
|
||||||
p.Pos.Column++
|
|
||||||
escaping := false
|
|
||||||
Byte:
|
|
||||||
for i < len(buf) {
|
|
||||||
b := buf[i]
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case b == '\\':
|
|
||||||
escaping = !escaping
|
|
||||||
p.Pos.Byte++
|
|
||||||
p.Pos.Column++
|
|
||||||
i++
|
|
||||||
case b == '"':
|
|
||||||
p.Pos.Byte++
|
|
||||||
p.Pos.Column++
|
|
||||||
i++
|
|
||||||
if !escaping {
|
|
||||||
break Byte
|
|
||||||
}
|
|
||||||
escaping = false
|
|
||||||
case b < 32:
|
|
||||||
break Byte
|
|
||||||
default:
|
|
||||||
// Advance by one grapheme cluster, so that we consider each
|
|
||||||
// grapheme to be a "column".
|
|
||||||
// Ignoring error because this scanner cannot produce errors.
|
|
||||||
advance, _, _ := textseg.ScanGraphemeClusters(buf[i:], true)
|
|
||||||
|
|
||||||
p.Pos.Byte += advance
|
|
||||||
p.Pos.Column++
|
|
||||||
i += advance
|
|
||||||
|
|
||||||
escaping = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf[:i], buf[i:], p
|
|
||||||
}
|
|
||||||
|
|
||||||
func skipWhitespace(buf []byte, start pos) ([]byte, pos) {
|
|
||||||
var i int
|
|
||||||
p := start
|
|
||||||
Byte:
|
|
||||||
for i = 0; i < len(buf); i++ {
|
|
||||||
switch buf[i] {
|
|
||||||
case ' ':
|
|
||||||
p.Pos.Byte++
|
|
||||||
p.Pos.Column++
|
|
||||||
case '\n':
|
|
||||||
p.Pos.Byte++
|
|
||||||
p.Pos.Column = 1
|
|
||||||
p.Pos.Line++
|
|
||||||
case '\r':
|
|
||||||
// For the purpose of line/column counting we consider a
|
|
||||||
// carriage return to take up no space, assuming that it will
|
|
||||||
// be paired up with a newline (on Windows, for example) that
|
|
||||||
// will account for both of them.
|
|
||||||
p.Pos.Byte++
|
|
||||||
case '\t':
|
|
||||||
// We arbitrarily count a tab as if it were two spaces, because
|
|
||||||
// we need to choose _some_ number here. This means any system
|
|
||||||
// that renders code on-screen with markers must itself treat
|
|
||||||
// tabs as a pair of spaces for rendering purposes, or instead
|
|
||||||
// use the byte offset and back into its own column position.
|
|
||||||
p.Pos.Byte++
|
|
||||||
p.Pos.Column += 2
|
|
||||||
default:
|
|
||||||
break Byte
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf[i:], p
|
|
||||||
}
|
|
||||||
|
|
||||||
type pos struct {
|
|
||||||
Filename string
|
|
||||||
Pos hcl.Pos
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pos) Range(byteLen, charLen int) hcl.Range {
|
|
||||||
start := p.Pos
|
|
||||||
end := p.Pos
|
|
||||||
end.Byte += byteLen
|
|
||||||
end.Column += charLen
|
|
||||||
return hcl.Range{
|
|
||||||
Filename: p.Filename,
|
|
||||||
Start: start,
|
|
||||||
End: end,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func posRange(start, end pos) hcl.Range {
|
|
||||||
return hcl.Range{
|
|
||||||
Filename: start.Filename,
|
|
||||||
Start: start.Pos,
|
|
||||||
End: end.Pos,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t token) GoString() string {
|
|
||||||
return fmt.Sprintf("json.token{json.%s, []byte(%q), %#v}", t.Type, t.Bytes, t.Range)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAlphabetical(b byte) bool {
|
|
||||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
|
|
||||||
}
|
|
|
@ -1,405 +0,0 @@
|
||||||
# HCL JSON Syntax Specification
|
|
||||||
|
|
||||||
This is the specification for the JSON serialization for hcl. HCL is a system
|
|
||||||
for defining configuration languages for applications. The HCL information
|
|
||||||
model is designed to support multiple concrete syntaxes for configuration,
|
|
||||||
and this JSON-based format complements [the native syntax](../hclsyntax/spec.md)
|
|
||||||
by being easy to machine-generate, whereas the native syntax is oriented
|
|
||||||
towards human authoring and maintenance
|
|
||||||
|
|
||||||
This syntax is defined in terms of JSON as defined in
|
|
||||||
[RFC7159](https://tools.ietf.org/html/rfc7159). As such it inherits the JSON
|
|
||||||
grammar as-is, and merely defines a specific methodology for interpreting
|
|
||||||
JSON constructs into HCL structural elements and expressions.
|
|
||||||
|
|
||||||
This mapping is defined such that valid JSON-serialized HCL input can be
|
|
||||||
_produced_ using standard JSON implementations in various programming languages.
|
|
||||||
_Parsing_ such JSON has some additional constraints not beyond what is normally
|
|
||||||
supported by JSON parsers, so a specialized parser may be required that
|
|
||||||
is able to:
|
|
||||||
|
|
||||||
- Preserve the relative ordering of properties defined in an object.
|
|
||||||
- Preserve multiple definitions of the same property name.
|
|
||||||
- Preserve numeric values to the precision required by the number type
|
|
||||||
in [the HCL syntax-agnostic information model](../spec.md).
|
|
||||||
- Retain source location information for parsed tokens/constructs in order
|
|
||||||
to produce good error messages.
|
|
||||||
|
|
||||||
## Structural Elements
|
|
||||||
|
|
||||||
[The HCL syntax-agnostic information model](../spec.md) defines a _body_ as an
|
|
||||||
abstract container for attribute definitions and child blocks. A body is
|
|
||||||
represented in JSON as either a single JSON object or a JSON array of objects.
|
|
||||||
|
|
||||||
Body processing is in terms of JSON object properties, visited in the order
|
|
||||||
they appear in the input. Where a body is represented by a single JSON object,
|
|
||||||
the properties of that object are visited in order. Where a body is
|
|
||||||
represented by a JSON array, each of its elements are visited in order and
|
|
||||||
each element has its properties visited in order. If any element of the array
|
|
||||||
is not a JSON object then the input is erroneous.
|
|
||||||
|
|
||||||
When a body is being processed in the _dynamic attributes_ mode, the allowance
|
|
||||||
of a JSON array in the previous paragraph does not apply and instead a single
|
|
||||||
JSON object is always required.
|
|
||||||
|
|
||||||
As defined in the language-agnostic model, body processing is in terms
|
|
||||||
of a schema which provides context for interpreting the body's content. For
|
|
||||||
JSON bodies, the schema is crucial to allow differentiation of attribute
|
|
||||||
definitions and block definitions, both of which are represented via object
|
|
||||||
properties.
|
|
||||||
|
|
||||||
The special property name `"//"`, when used in an object representing a HCL
|
|
||||||
body, is parsed and ignored. A property with this name can be used to
|
|
||||||
include human-readable comments. (This special property name is _not_
|
|
||||||
processed in this way for any _other_ HCL constructs that are represented as
|
|
||||||
JSON objects.)
|
|
||||||
|
|
||||||
### Attributes
|
|
||||||
|
|
||||||
Where the given schema describes an attribute with a given name, the object
|
|
||||||
property with the matching name — if present — serves as the attribute's
|
|
||||||
definition.
|
|
||||||
|
|
||||||
When a body is being processed in the _dynamic attributes_ mode, each object
|
|
||||||
property serves as an attribute definition for the attribute whose name
|
|
||||||
matches the property name.
|
|
||||||
|
|
||||||
The value of an attribute definition property is interpreted as an _expression_,
|
|
||||||
as described in a later section.
|
|
||||||
|
|
||||||
Given a schema that calls for an attribute named "foo", a JSON object like
|
|
||||||
the following provides a definition for that attribute:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"foo": "bar baz"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Blocks
|
|
||||||
|
|
||||||
Where the given schema describes a block with a given type name, each object
|
|
||||||
property with the matching name serves as a definition of zero or more blocks
|
|
||||||
of that type.
|
|
||||||
|
|
||||||
Processing of child blocks is in terms of nested JSON objects and arrays.
|
|
||||||
If the schema defines one or more _labels_ for the block type, a nested JSON
|
|
||||||
object or JSON array of objects is required for each labelling level. These
|
|
||||||
are flattened to a single ordered sequence of object properties using the
|
|
||||||
same algorithm as for body content as defined above. Each object property
|
|
||||||
serves as a label value at the corresponding level.
|
|
||||||
|
|
||||||
After any labelling levels, the next nested value is either a JSON object
|
|
||||||
representing a single block body, or a JSON array of JSON objects that each
|
|
||||||
represent a single block body. Use of an array accommodates the definition
|
|
||||||
of multiple blocks that have identical type and labels.
|
|
||||||
|
|
||||||
Given a schema that calls for a block type named "foo" with no labels, the
|
|
||||||
following JSON objects are all valid definitions of zero or more blocks of this
|
|
||||||
type:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"foo": {
|
|
||||||
"child_attr": "baz"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"foo": [
|
|
||||||
{
|
|
||||||
"child_attr": "baz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"child_attr": "boz"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"foo": []
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The first of these defines a single child block of type "foo". The second
|
|
||||||
defines _two_ such blocks. The final example shows a degenerate definition
|
|
||||||
of zero blocks, though generators should prefer to omit the property entirely
|
|
||||||
in this scenario.
|
|
||||||
|
|
||||||
Given a schema that calls for a block type named "foo" with _two_ labels, the
|
|
||||||
extra label levels must be represented as objects or arrays of objects as in
|
|
||||||
the following examples:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"foo": {
|
|
||||||
"bar": {
|
|
||||||
"baz": {
|
|
||||||
"child_attr": "baz"
|
|
||||||
},
|
|
||||||
"boz": {
|
|
||||||
"child_attr": "baz"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"boz": {
|
|
||||||
"baz": {
|
|
||||||
"child_attr": "baz"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"foo": {
|
|
||||||
"bar": {
|
|
||||||
"baz": {
|
|
||||||
"child_attr": "baz"
|
|
||||||
},
|
|
||||||
"boz": {
|
|
||||||
"child_attr": "baz"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"boz": {
|
|
||||||
"baz": [
|
|
||||||
{
|
|
||||||
"child_attr": "baz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"child_attr": "boz"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"foo": [
|
|
||||||
{
|
|
||||||
"bar": {
|
|
||||||
"baz": {
|
|
||||||
"child_attr": "baz"
|
|
||||||
},
|
|
||||||
"boz": {
|
|
||||||
"child_attr": "baz"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bar": {
|
|
||||||
"baz": [
|
|
||||||
{
|
|
||||||
"child_attr": "baz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"child_attr": "boz"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"foo": {
|
|
||||||
"bar": {
|
|
||||||
"baz": {
|
|
||||||
"child_attr": "baz"
|
|
||||||
},
|
|
||||||
"boz": {
|
|
||||||
"child_attr": "baz"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bar": {
|
|
||||||
"baz": [
|
|
||||||
{
|
|
||||||
"child_attr": "baz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"child_attr": "boz"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Arrays can be introduced at either the label definition or block body
|
|
||||||
definition levels to define multiple definitions of the same block type
|
|
||||||
or labels while preserving order.
|
|
||||||
|
|
||||||
A JSON HCL parser _must_ support duplicate definitions of the same property
|
|
||||||
name within a single object, preserving all of them and the relative ordering
|
|
||||||
between them. The array-based forms are also required so that JSON HCL
|
|
||||||
configurations can be produced with JSON producing libraries that are not
|
|
||||||
able to preserve property definition order and multiple definitions of
|
|
||||||
the same property.
|
|
||||||
|
|
||||||
## Expressions
|
|
||||||
|
|
||||||
JSON lacks a native expression syntax, so the HCL JSON syntax instead defines
|
|
||||||
a mapping for each of the JSON value types, including a special mapping for
|
|
||||||
strings that allows optional use of arbitrary expressions.
|
|
||||||
|
|
||||||
### Objects
|
|
||||||
|
|
||||||
When interpreted as an expression, a JSON object represents a value of a HCL
|
|
||||||
object type.
|
|
||||||
|
|
||||||
Each property of the JSON object represents an attribute of the HCL object type.
|
|
||||||
The property name string given in the JSON input is interpreted as a string
|
|
||||||
expression as described below, and its result is converted to string as defined
|
|
||||||
by the syntax-agnostic information model. If such a conversion is not possible,
|
|
||||||
an error is produced and evaluation fails.
|
|
||||||
|
|
||||||
An instance of the constructed object type is then created, whose values
|
|
||||||
are interpreted by again recursively applying the mapping rules defined in
|
|
||||||
this section to each of the property values.
|
|
||||||
|
|
||||||
If any evaluated property name strings produce null values, an error is
|
|
||||||
produced and evaluation fails. If any produce _unknown_ values, the _entire
|
|
||||||
object's_ result is an unknown value of the dynamic pseudo-type, signalling
|
|
||||||
that the type of the object cannot be determined.
|
|
||||||
|
|
||||||
It is an error to define the same property name multiple times within a single
|
|
||||||
JSON object interpreted as an expression. In full expression mode, this
|
|
||||||
constraint applies to the name expression results after conversion to string,
|
|
||||||
rather than the raw string that may contain interpolation expressions.
|
|
||||||
|
|
||||||
### Arrays
|
|
||||||
|
|
||||||
When interpreted as an expression, a JSON array represents a value of a HCL
|
|
||||||
tuple type.
|
|
||||||
|
|
||||||
Each element of the JSON array represents an element of the HCL tuple type.
|
|
||||||
The tuple type is constructed by enumerating the JSON array elements, creating
|
|
||||||
for each an element whose type is the result of recursively applying the
|
|
||||||
expression mapping rules. Correspondence is preserved between the array element
|
|
||||||
indices and the tuple element indices.
|
|
||||||
|
|
||||||
An instance of the constructed tuple type is then created, whose values are
|
|
||||||
interpreted by again recursively applying the mapping rules defined in this
|
|
||||||
section.
|
|
||||||
|
|
||||||
### Numbers
|
|
||||||
|
|
||||||
When interpreted as an expression, a JSON number represents a HCL number value.
|
|
||||||
|
|
||||||
HCL numbers are arbitrary-precision decimal values, so a JSON HCL parser must
|
|
||||||
be able to translate exactly the value given to a number of corresponding
|
|
||||||
precision, within the constraints set by the HCL syntax-agnostic information
|
|
||||||
model.
|
|
||||||
|
|
||||||
In practice, off-the-shelf JSON serializers often do not support customizing the
|
|
||||||
processing of numbers, and instead force processing as 32-bit or 64-bit
|
|
||||||
floating point values.
|
|
||||||
|
|
||||||
A _producer_ of JSON HCL that uses such a serializer can provide numeric values
|
|
||||||
as JSON strings where they have precision too great for representation in the
|
|
||||||
serializer's chosen numeric type in situations where the result will be
|
|
||||||
converted to number (using the standard conversion rules) by a calling
|
|
||||||
application.
|
|
||||||
|
|
||||||
Alternatively, for expressions that are evaluated in full expression mode an
|
|
||||||
embedded template interpolation can be used to faithfully represent a number,
|
|
||||||
such as `"${1e150}"`, which will then be evaluated by the underlying HCL native
|
|
||||||
syntax expression evaluator.
|
|
||||||
|
|
||||||
### Boolean Values
|
|
||||||
|
|
||||||
The JSON boolean values `true` and `false`, when interpreted as expressions,
|
|
||||||
represent the corresponding HCL boolean values.
|
|
||||||
|
|
||||||
### The Null Value
|
|
||||||
|
|
||||||
The JSON value `null`, when interpreted as an expression, represents a
|
|
||||||
HCL null value of the dynamic pseudo-type.
|
|
||||||
|
|
||||||
### Strings
|
|
||||||
|
|
||||||
When interpreted as an expression, a JSON string may be interpreted in one of
|
|
||||||
two ways depending on the evaluation mode.
|
|
||||||
|
|
||||||
If evaluating in literal-only mode (as defined by the syntax-agnostic
|
|
||||||
information model) the literal string is intepreted directly as a HCL string
|
|
||||||
value, by directly using the exact sequence of unicode characters represented.
|
|
||||||
Template interpolations and directives MUST NOT be processed in this mode,
|
|
||||||
allowing any characters that appear as introduction sequences to pass through
|
|
||||||
literally:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"Hello world! Template sequences like ${ are not intepreted here."
|
|
||||||
```
|
|
||||||
|
|
||||||
When evaluating in full expression mode (again, as defined by the syntax-
|
|
||||||
agnostic information model) the literal string is instead interpreted as a
|
|
||||||
_standalone template_ in the HCL Native Syntax. The expression evaluation
|
|
||||||
result is then the direct result of evaluating that template with the current
|
|
||||||
variable scope and function table.
|
|
||||||
|
|
||||||
```json
|
|
||||||
"Hello, ${name}! Template sequences are interpreted in full expression mode."
|
|
||||||
```
|
|
||||||
|
|
||||||
In particular the _Template Interpolation Unwrapping_ requirement from the
|
|
||||||
HCL native syntax specification must be implemented, allowing the use of
|
|
||||||
single-interpolation templates to represent expressions that would not
|
|
||||||
otherwise be representable in JSON, such as the following example where
|
|
||||||
the result must be a number, rather than a string representation of a number:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"${ a + b }"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Static Analysis
|
|
||||||
|
|
||||||
The HCL static analysis operations are implemented for JSON values that
|
|
||||||
represent expressions, as described in the following sections.
|
|
||||||
|
|
||||||
Due to the limited expressive power of the JSON syntax alone, use of these
|
|
||||||
static analyses functions rather than normal expression evaluation is used
|
|
||||||
as additional context for how a JSON value is to be interpreted, which means
|
|
||||||
that static analyses can result in a different interpretation of a given
|
|
||||||
expression than normal evaluation.
|
|
||||||
|
|
||||||
### Static List
|
|
||||||
|
|
||||||
An expression interpreted as a static list must be a JSON array. Each of the
|
|
||||||
values in the array is interpreted as an expression and returned.
|
|
||||||
|
|
||||||
### Static Map
|
|
||||||
|
|
||||||
An expression interpreted as a static map must be a JSON object. Each of the
|
|
||||||
key/value pairs in the object is presented as a pair of expressions. Since
|
|
||||||
object property names are always strings, evaluating the key expression with
|
|
||||||
a non-`nil` evaluation context will evaluate any template sequences given
|
|
||||||
in the property name.
|
|
||||||
|
|
||||||
### Static Call
|
|
||||||
|
|
||||||
An expression interpreted as a static call must be a string. The content of
|
|
||||||
the string is interpreted as a native syntax expression (not a _template_,
|
|
||||||
unlike normal evaluation) and then the static call analysis is delegated to
|
|
||||||
that expression.
|
|
||||||
|
|
||||||
If the original expression is not a string or its contents cannot be parsed
|
|
||||||
as a native syntax expression then static call analysis is not supported.
|
|
||||||
|
|
||||||
### Static Traversal
|
|
||||||
|
|
||||||
An expression interpreted as a static traversal must be a string. The content
|
|
||||||
of the string is interpreted as a native syntax expression (not a _template_,
|
|
||||||
unlike normal evaluation) and then static traversal analysis is delegated
|
|
||||||
to that expression.
|
|
||||||
|
|
||||||
If the original expression is not a string or its contents cannot be parsed
|
|
||||||
as a native syntax expression then static call analysis is not supported.
|
|
|
@ -1,637 +0,0 @@
|
||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
"github.com/zclconf/go-cty/cty/convert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// body is the implementation of "Body" used for files processed with the JSON
|
|
||||||
// parser.
|
|
||||||
type body struct {
|
|
||||||
val node
|
|
||||||
|
|
||||||
// If non-nil, the keys of this map cause the corresponding attributes to
|
|
||||||
// be treated as non-existing. This is used when Body.PartialContent is
|
|
||||||
// called, to produce the "remaining content" Body.
|
|
||||||
hiddenAttrs map[string]struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// expression is the implementation of "Expression" used for files processed
|
|
||||||
// with the JSON parser.
|
|
||||||
type expression struct {
|
|
||||||
src node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
|
|
||||||
content, newBody, diags := b.PartialContent(schema)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonAttrs, attrDiags := b.collectDeepAttrs(b.val, nil)
|
|
||||||
diags = append(diags, attrDiags...)
|
|
||||||
|
|
||||||
for _, attr := range jsonAttrs {
|
|
||||||
k := attr.Name
|
|
||||||
if k == "//" {
|
|
||||||
// Ignore "//" keys in objects representing bodies, to allow
|
|
||||||
// their use as comments.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := hiddenAttrs[k]; !ok {
|
|
||||||
suggestion := nameSuggestion(k, nameSuggestions)
|
|
||||||
if suggestion != "" {
|
|
||||||
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
|
||||||
}
|
|
||||||
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Extraneous JSON object property",
|
|
||||||
Detail: fmt.Sprintf("No argument or block type is named %q.%s", k, suggestion),
|
|
||||||
Subject: &attr.NameRange,
|
|
||||||
Context: attr.Range().Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return content, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
|
|
||||||
jsonAttrs, attrDiags := b.collectDeepAttrs(b.val, nil)
|
|
||||||
diags = append(diags, attrDiags...)
|
|
||||||
|
|
||||||
usedNames := map[string]struct{}{}
|
|
||||||
if b.hiddenAttrs != nil {
|
|
||||||
for k := range b.hiddenAttrs {
|
|
||||||
usedNames[k] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content := &hcl.BodyContent{
|
|
||||||
Attributes: map[string]*hcl.Attribute{},
|
|
||||||
Blocks: nil,
|
|
||||||
|
|
||||||
MissingItemRange: b.MissingItemRange(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create some more convenient data structures for our work below.
|
|
||||||
attrSchemas := map[string]hcl.AttributeSchema{}
|
|
||||||
blockSchemas := map[string]hcl.BlockHeaderSchema{}
|
|
||||||
for _, attrS := range schema.Attributes {
|
|
||||||
attrSchemas[attrS.Name] = attrS
|
|
||||||
}
|
|
||||||
for _, blockS := range schema.Blocks {
|
|
||||||
blockSchemas[blockS.Type] = blockS
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, jsonAttr := range jsonAttrs {
|
|
||||||
attrName := jsonAttr.Name
|
|
||||||
if _, used := b.hiddenAttrs[attrName]; used {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if attrS, defined := attrSchemas[attrName]; defined {
|
|
||||||
if existing, exists := content.Attributes[attrName]; exists {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Duplicate argument",
|
|
||||||
Detail: fmt.Sprintf("The argument %q was already set at %s.", attrName, existing.Range),
|
|
||||||
Subject: &jsonAttr.NameRange,
|
|
||||||
Context: jsonAttr.Range().Ptr(),
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
content.Attributes[attrS.Name] = &hcl.Attribute{
|
|
||||||
Name: attrS.Name,
|
|
||||||
Expr: &expression{src: jsonAttr.Value},
|
|
||||||
Range: hcl.RangeBetween(jsonAttr.NameRange, jsonAttr.Value.Range()),
|
|
||||||
NameRange: jsonAttr.NameRange,
|
|
||||||
}
|
|
||||||
usedNames[attrName] = struct{}{}
|
|
||||||
|
|
||||||
} else if blockS, defined := blockSchemas[attrName]; defined {
|
|
||||||
bv := jsonAttr.Value
|
|
||||||
blockDiags := b.unpackBlock(bv, blockS.Type, &jsonAttr.NameRange, blockS.LabelNames, nil, nil, &content.Blocks)
|
|
||||||
diags = append(diags, blockDiags...)
|
|
||||||
usedNames[attrName] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We ignore anything that isn't defined because that's the
|
|
||||||
// PartialContent contract. The Content method will catch leftovers.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we got all the required attributes.
|
|
||||||
for _, attrS := range schema.Attributes {
|
|
||||||
if !attrS.Required {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, defined := content.Attributes[attrS.Name]; !defined {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Missing required argument",
|
|
||||||
Detail: fmt.Sprintf("The argument %q is required, but no definition was found.", attrS.Name),
|
|
||||||
Subject: b.MissingItemRange().Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unusedBody := &body{
|
|
||||||
val: b.val,
|
|
||||||
hiddenAttrs: usedNames,
|
|
||||||
}
|
|
||||||
|
|
||||||
return content, unusedBody, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// JustAttributes for JSON bodies interprets all properties of the wrapped
|
|
||||||
// JSON object as attributes and returns them.
|
|
||||||
func (b *body) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
attrs := make(map[string]*hcl.Attribute)
|
|
||||||
|
|
||||||
obj, ok := b.val.(*objectVal)
|
|
||||||
if !ok {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Incorrect JSON value type",
|
|
||||||
Detail: "A JSON object is required here, setting the arguments for this block.",
|
|
||||||
Subject: b.val.StartRange().Ptr(),
|
|
||||||
})
|
|
||||||
return attrs, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, jsonAttr := range obj.Attrs {
|
|
||||||
name := jsonAttr.Name
|
|
||||||
if name == "//" {
|
|
||||||
// Ignore "//" keys in objects representing bodies, to allow
|
|
||||||
// their use as comments.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, hidden := b.hiddenAttrs[name]; hidden {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if existing, exists := attrs[name]; exists {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Duplicate attribute definition",
|
|
||||||
Detail: fmt.Sprintf("The argument %q was already set at %s.", name, existing.Range),
|
|
||||||
Subject: &jsonAttr.NameRange,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs[name] = &hcl.Attribute{
|
|
||||||
Name: name,
|
|
||||||
Expr: &expression{src: jsonAttr.Value},
|
|
||||||
Range: hcl.RangeBetween(jsonAttr.NameRange, jsonAttr.Value.Range()),
|
|
||||||
NameRange: jsonAttr.NameRange,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No diagnostics possible here, since the parser already took care of
|
|
||||||
// finding duplicates and every JSON value can be a valid attribute value.
|
|
||||||
return attrs, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *body) MissingItemRange() hcl.Range {
|
|
||||||
switch tv := b.val.(type) {
|
|
||||||
case *objectVal:
|
|
||||||
return tv.CloseRange
|
|
||||||
case *arrayVal:
|
|
||||||
return tv.OpenRange
|
|
||||||
default:
|
|
||||||
// Should not happen in correct operation, but might show up if the
|
|
||||||
// input is invalid and we are producing partial results.
|
|
||||||
return tv.StartRange()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *body) unpackBlock(v node, typeName string, typeRange *hcl.Range, labelsLeft []string, labelsUsed []string, labelRanges []hcl.Range, blocks *hcl.Blocks) (diags hcl.Diagnostics) {
|
|
||||||
if len(labelsLeft) > 0 {
|
|
||||||
labelName := labelsLeft[0]
|
|
||||||
jsonAttrs, attrDiags := b.collectDeepAttrs(v, &labelName)
|
|
||||||
diags = append(diags, attrDiags...)
|
|
||||||
|
|
||||||
if len(jsonAttrs) == 0 {
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Missing block label",
|
|
||||||
Detail: fmt.Sprintf("At least one object property is required, whose name represents the %s block's %s.", typeName, labelName),
|
|
||||||
Subject: v.StartRange().Ptr(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
labelsUsed := append(labelsUsed, "")
|
|
||||||
labelRanges := append(labelRanges, hcl.Range{})
|
|
||||||
for _, p := range jsonAttrs {
|
|
||||||
pk := p.Name
|
|
||||||
labelsUsed[len(labelsUsed)-1] = pk
|
|
||||||
labelRanges[len(labelRanges)-1] = p.NameRange
|
|
||||||
diags = append(diags, b.unpackBlock(p.Value, typeName, typeRange, labelsLeft[1:], labelsUsed, labelRanges, blocks)...)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// By the time we get here, we've peeled off all the labels and we're ready
|
|
||||||
// to deal with the block's actual content.
|
|
||||||
|
|
||||||
// need to copy the label slices because their underlying arrays will
|
|
||||||
// continue to be mutated after we return.
|
|
||||||
labels := make([]string, len(labelsUsed))
|
|
||||||
copy(labels, labelsUsed)
|
|
||||||
labelR := make([]hcl.Range, len(labelRanges))
|
|
||||||
copy(labelR, labelRanges)
|
|
||||||
|
|
||||||
switch tv := v.(type) {
|
|
||||||
case *nullVal:
|
|
||||||
// There is no block content, e.g the value is null.
|
|
||||||
return
|
|
||||||
case *objectVal:
|
|
||||||
// Single instance of the block
|
|
||||||
*blocks = append(*blocks, &hcl.Block{
|
|
||||||
Type: typeName,
|
|
||||||
Labels: labels,
|
|
||||||
Body: &body{
|
|
||||||
val: tv,
|
|
||||||
},
|
|
||||||
|
|
||||||
DefRange: tv.OpenRange,
|
|
||||||
TypeRange: *typeRange,
|
|
||||||
LabelRanges: labelR,
|
|
||||||
})
|
|
||||||
case *arrayVal:
|
|
||||||
// Multiple instances of the block
|
|
||||||
for _, av := range tv.Values {
|
|
||||||
*blocks = append(*blocks, &hcl.Block{
|
|
||||||
Type: typeName,
|
|
||||||
Labels: labels,
|
|
||||||
Body: &body{
|
|
||||||
val: av, // might be mistyped; we'll find out when content is requested for this body
|
|
||||||
},
|
|
||||||
|
|
||||||
DefRange: tv.OpenRange,
|
|
||||||
TypeRange: *typeRange,
|
|
||||||
LabelRanges: labelR,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Incorrect JSON value type",
|
|
||||||
Detail: fmt.Sprintf("Either a JSON object or a JSON array is required, representing the contents of one or more %q blocks.", typeName),
|
|
||||||
Subject: v.StartRange().Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// collectDeepAttrs takes either a single object or an array of objects and
|
|
||||||
// flattens it into a list of object attributes, collecting attributes from
|
|
||||||
// all of the objects in a given array.
|
|
||||||
//
|
|
||||||
// Ordering is preserved, so a list of objects that each have one property
|
|
||||||
// will result in those properties being returned in the same order as the
|
|
||||||
// objects appeared in the array.
|
|
||||||
//
|
|
||||||
// This is appropriate for use only for objects representing bodies or labels
|
|
||||||
// within a block.
|
|
||||||
//
|
|
||||||
// The labelName argument, if non-null, is used to tailor returned error
|
|
||||||
// messages to refer to block labels rather than attributes and child blocks.
|
|
||||||
// It has no other effect.
|
|
||||||
func (b *body) collectDeepAttrs(v node, labelName *string) ([]*objectAttr, hcl.Diagnostics) {
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
var attrs []*objectAttr
|
|
||||||
|
|
||||||
switch tv := v.(type) {
|
|
||||||
case *nullVal:
|
|
||||||
// If a value is null, then we don't return any attributes or return an error.
|
|
||||||
|
|
||||||
case *objectVal:
|
|
||||||
attrs = append(attrs, tv.Attrs...)
|
|
||||||
|
|
||||||
case *arrayVal:
|
|
||||||
for _, ev := range tv.Values {
|
|
||||||
switch tev := ev.(type) {
|
|
||||||
case *objectVal:
|
|
||||||
attrs = append(attrs, tev.Attrs...)
|
|
||||||
default:
|
|
||||||
if labelName != nil {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Incorrect JSON value type",
|
|
||||||
Detail: fmt.Sprintf("A JSON object is required here, to specify %s labels for this block.", *labelName),
|
|
||||||
Subject: ev.StartRange().Ptr(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Incorrect JSON value type",
|
|
||||||
Detail: "A JSON object is required here, to define arguments and child blocks.",
|
|
||||||
Subject: ev.StartRange().Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
if labelName != nil {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Incorrect JSON value type",
|
|
||||||
Detail: fmt.Sprintf("Either a JSON object or JSON array of objects is required here, to specify %s labels for this block.", *labelName),
|
|
||||||
Subject: v.StartRange().Ptr(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Incorrect JSON value type",
|
|
||||||
Detail: "Either a JSON object or JSON array of objects is required here, to define arguments and child blocks.",
|
|
||||||
Subject: v.StartRange().Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *expression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
||||||
switch v := e.src.(type) {
|
|
||||||
case *stringVal:
|
|
||||||
if ctx != nil {
|
|
||||||
// Parse string contents as a HCL native language expression.
|
|
||||||
// We only do this if we have a context, so passing a nil context
|
|
||||||
// is how the caller specifies that interpolations are not allowed
|
|
||||||
// and that the string should just be returned verbatim.
|
|
||||||
templateSrc := v.Value
|
|
||||||
expr, diags := hclsyntax.ParseTemplate(
|
|
||||||
[]byte(templateSrc),
|
|
||||||
v.SrcRange.Filename,
|
|
||||||
|
|
||||||
// This won't produce _exactly_ the right result, since
|
|
||||||
// the hclsyntax parser can't "see" any escapes we removed
|
|
||||||
// while parsing JSON, but it's better than nothing.
|
|
||||||
hcl.Pos{
|
|
||||||
Line: v.SrcRange.Start.Line,
|
|
||||||
|
|
||||||
// skip over the opening quote mark
|
|
||||||
Byte: v.SrcRange.Start.Byte + 1,
|
|
||||||
Column: v.SrcRange.Start.Column + 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return cty.DynamicVal, diags
|
|
||||||
}
|
|
||||||
val, evalDiags := expr.Value(ctx)
|
|
||||||
diags = append(diags, evalDiags...)
|
|
||||||
return val, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
return cty.StringVal(v.Value), nil
|
|
||||||
case *numberVal:
|
|
||||||
return cty.NumberVal(v.Value), nil
|
|
||||||
case *booleanVal:
|
|
||||||
return cty.BoolVal(v.Value), nil
|
|
||||||
case *arrayVal:
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
vals := []cty.Value{}
|
|
||||||
for _, jsonVal := range v.Values {
|
|
||||||
val, valDiags := (&expression{src: jsonVal}).Value(ctx)
|
|
||||||
vals = append(vals, val)
|
|
||||||
diags = append(diags, valDiags...)
|
|
||||||
}
|
|
||||||
return cty.TupleVal(vals), diags
|
|
||||||
case *objectVal:
|
|
||||||
var diags hcl.Diagnostics
|
|
||||||
attrs := map[string]cty.Value{}
|
|
||||||
attrRanges := map[string]hcl.Range{}
|
|
||||||
known := true
|
|
||||||
for _, jsonAttr := range v.Attrs {
|
|
||||||
// In this one context we allow keys to contain interpolation
|
|
||||||
// expressions too, assuming we're evaluating in interpolation
|
|
||||||
// mode. This achieves parity with the native syntax where
|
|
||||||
// object expressions can have dynamic keys, while block contents
|
|
||||||
// may not.
|
|
||||||
name, nameDiags := (&expression{src: &stringVal{
|
|
||||||
Value: jsonAttr.Name,
|
|
||||||
SrcRange: jsonAttr.NameRange,
|
|
||||||
}}).Value(ctx)
|
|
||||||
valExpr := &expression{src: jsonAttr.Value}
|
|
||||||
val, valDiags := valExpr.Value(ctx)
|
|
||||||
diags = append(diags, nameDiags...)
|
|
||||||
diags = append(diags, valDiags...)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
name, err = convert.Convert(name, cty.String)
|
|
||||||
if err != nil {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid object key expression",
|
|
||||||
Detail: fmt.Sprintf("Cannot use this expression as an object key: %s.", err),
|
|
||||||
Subject: &jsonAttr.NameRange,
|
|
||||||
Expression: valExpr,
|
|
||||||
EvalContext: ctx,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if name.IsNull() {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid object key expression",
|
|
||||||
Detail: "Cannot use null value as an object key.",
|
|
||||||
Subject: &jsonAttr.NameRange,
|
|
||||||
Expression: valExpr,
|
|
||||||
EvalContext: ctx,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !name.IsKnown() {
|
|
||||||
// This is a bit of a weird case, since our usual rules require
|
|
||||||
// us to tolerate unknowns and just represent the result as
|
|
||||||
// best we can but if we don't know the key then we can't
|
|
||||||
// know the type of our object at all, and thus we must turn
|
|
||||||
// the whole thing into cty.DynamicVal. This is consistent with
|
|
||||||
// how this situation is handled in the native syntax.
|
|
||||||
// We'll keep iterating so we can collect other errors in
|
|
||||||
// subsequent attributes.
|
|
||||||
known = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nameStr := name.AsString()
|
|
||||||
if _, defined := attrs[nameStr]; defined {
|
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Duplicate object attribute",
|
|
||||||
Detail: fmt.Sprintf("An attribute named %q was already defined at %s.", nameStr, attrRanges[nameStr]),
|
|
||||||
Subject: &jsonAttr.NameRange,
|
|
||||||
Expression: e,
|
|
||||||
EvalContext: ctx,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
attrs[nameStr] = val
|
|
||||||
attrRanges[nameStr] = jsonAttr.NameRange
|
|
||||||
}
|
|
||||||
if !known {
|
|
||||||
// We encountered an unknown key somewhere along the way, so
|
|
||||||
// we can't know what our type will eventually be.
|
|
||||||
return cty.DynamicVal, diags
|
|
||||||
}
|
|
||||||
return cty.ObjectVal(attrs), diags
|
|
||||||
case *nullVal:
|
|
||||||
return cty.NullVal(cty.DynamicPseudoType), nil
|
|
||||||
default:
|
|
||||||
// Default to DynamicVal so that ASTs containing invalid nodes can
|
|
||||||
// still be partially-evaluated.
|
|
||||||
return cty.DynamicVal, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *expression) Variables() []hcl.Traversal {
|
|
||||||
var vars []hcl.Traversal
|
|
||||||
|
|
||||||
switch v := e.src.(type) {
|
|
||||||
case *stringVal:
|
|
||||||
templateSrc := v.Value
|
|
||||||
expr, diags := hclsyntax.ParseTemplate(
|
|
||||||
[]byte(templateSrc),
|
|
||||||
v.SrcRange.Filename,
|
|
||||||
|
|
||||||
// This won't produce _exactly_ the right result, since
|
|
||||||
// the hclsyntax parser can't "see" any escapes we removed
|
|
||||||
// while parsing JSON, but it's better than nothing.
|
|
||||||
hcl.Pos{
|
|
||||||
Line: v.SrcRange.Start.Line,
|
|
||||||
|
|
||||||
// skip over the opening quote mark
|
|
||||||
Byte: v.SrcRange.Start.Byte + 1,
|
|
||||||
Column: v.SrcRange.Start.Column + 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return vars
|
|
||||||
}
|
|
||||||
return expr.Variables()
|
|
||||||
|
|
||||||
case *arrayVal:
|
|
||||||
for _, jsonVal := range v.Values {
|
|
||||||
vars = append(vars, (&expression{src: jsonVal}).Variables()...)
|
|
||||||
}
|
|
||||||
case *objectVal:
|
|
||||||
for _, jsonAttr := range v.Attrs {
|
|
||||||
keyExpr := &stringVal{ // we're going to treat key as an expression in this context
|
|
||||||
Value: jsonAttr.Name,
|
|
||||||
SrcRange: jsonAttr.NameRange,
|
|
||||||
}
|
|
||||||
vars = append(vars, (&expression{src: keyExpr}).Variables()...)
|
|
||||||
vars = append(vars, (&expression{src: jsonAttr.Value}).Variables()...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return vars
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *expression) Range() hcl.Range {
|
|
||||||
return e.src.Range()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *expression) StartRange() hcl.Range {
|
|
||||||
return e.src.StartRange()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation for hcl.AbsTraversalForExpr.
|
|
||||||
func (e *expression) AsTraversal() hcl.Traversal {
|
|
||||||
// In JSON-based syntax a traversal is given as a string containing
|
|
||||||
// traversal syntax as defined by hclsyntax.ParseTraversalAbs.
|
|
||||||
|
|
||||||
switch v := e.src.(type) {
|
|
||||||
case *stringVal:
|
|
||||||
traversal, diags := hclsyntax.ParseTraversalAbs([]byte(v.Value), v.SrcRange.Filename, v.SrcRange.Start)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return traversal
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation for hcl.ExprCall.
|
|
||||||
func (e *expression) ExprCall() *hcl.StaticCall {
|
|
||||||
// In JSON-based syntax a static call is given as a string containing
|
|
||||||
// an expression in the native syntax that also supports ExprCall.
|
|
||||||
|
|
||||||
switch v := e.src.(type) {
|
|
||||||
case *stringVal:
|
|
||||||
expr, diags := hclsyntax.ParseExpression([]byte(v.Value), v.SrcRange.Filename, v.SrcRange.Start)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
call, diags := hcl.ExprCall(expr)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return call
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation for hcl.ExprList.
|
|
||||||
func (e *expression) ExprList() []hcl.Expression {
|
|
||||||
switch v := e.src.(type) {
|
|
||||||
case *arrayVal:
|
|
||||||
ret := make([]hcl.Expression, len(v.Values))
|
|
||||||
for i, node := range v.Values {
|
|
||||||
ret[i] = &expression{src: node}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation for hcl.ExprMap.
|
|
||||||
func (e *expression) ExprMap() []hcl.KeyValuePair {
|
|
||||||
switch v := e.src.(type) {
|
|
||||||
case *objectVal:
|
|
||||||
ret := make([]hcl.KeyValuePair, len(v.Attrs))
|
|
||||||
for i, jsonAttr := range v.Attrs {
|
|
||||||
ret[i] = hcl.KeyValuePair{
|
|
||||||
Key: &expression{src: &stringVal{
|
|
||||||
Value: jsonAttr.Name,
|
|
||||||
SrcRange: jsonAttr.NameRange,
|
|
||||||
}},
|
|
||||||
Value: &expression{src: jsonAttr.Value},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
// Code generated by "stringer -type tokenType scanner.go"; DO NOT EDIT.
|
|
||||||
|
|
||||||
package json
|
|
||||||
|
|
||||||
import "strconv"
|
|
||||||
|
|
||||||
const _tokenType_name = "tokenInvalidtokenCommatokenColontokenEqualstokenKeywordtokenNumbertokenStringtokenBrackOtokenBrackCtokenBraceOtokenBraceCtokenEOF"
|
|
||||||
|
|
||||||
var _tokenType_map = map[tokenType]string{
|
|
||||||
0: _tokenType_name[0:12],
|
|
||||||
44: _tokenType_name[12:22],
|
|
||||||
58: _tokenType_name[22:32],
|
|
||||||
61: _tokenType_name[32:43],
|
|
||||||
75: _tokenType_name[43:55],
|
|
||||||
78: _tokenType_name[55:66],
|
|
||||||
83: _tokenType_name[66:77],
|
|
||||||
91: _tokenType_name[77:88],
|
|
||||||
93: _tokenType_name[88:99],
|
|
||||||
123: _tokenType_name[99:110],
|
|
||||||
125: _tokenType_name[110:121],
|
|
||||||
9220: _tokenType_name[121:129],
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i tokenType) String() string {
|
|
||||||
if str, ok := _tokenType_map[i]; ok {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
return "tokenType(" + strconv.FormatInt(int64(i), 10) + ")"
|
|
||||||
}
|
|
|
@ -1,226 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MergeFiles combines the given files to produce a single body that contains
|
|
||||||
// configuration from all of the given files.
|
|
||||||
//
|
|
||||||
// The ordering of the given files decides the order in which contained
|
|
||||||
// elements will be returned. If any top-level attributes are defined with
|
|
||||||
// the same name across multiple files, a diagnostic will be produced from
|
|
||||||
// the Content and PartialContent methods describing this error in a
|
|
||||||
// user-friendly way.
|
|
||||||
func MergeFiles(files []*File) Body {
|
|
||||||
var bodies []Body
|
|
||||||
for _, file := range files {
|
|
||||||
bodies = append(bodies, file.Body)
|
|
||||||
}
|
|
||||||
return MergeBodies(bodies)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeBodies is like MergeFiles except it deals directly with bodies, rather
|
|
||||||
// than with entire files.
|
|
||||||
func MergeBodies(bodies []Body) Body {
|
|
||||||
if len(bodies) == 0 {
|
|
||||||
// Swap out for our singleton empty body, to reduce the number of
|
|
||||||
// empty slices we have hanging around.
|
|
||||||
return emptyBody
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any of the given bodies are already merged bodies, we'll unpack
|
|
||||||
// to flatten to a single mergedBodies, since that's conceptually simpler.
|
|
||||||
// This also, as a side-effect, eliminates any empty bodies, since
|
|
||||||
// empties are merged bodies with no inner bodies.
|
|
||||||
var newLen int
|
|
||||||
var flatten bool
|
|
||||||
for _, body := range bodies {
|
|
||||||
if children, merged := body.(mergedBodies); merged {
|
|
||||||
newLen += len(children)
|
|
||||||
flatten = true
|
|
||||||
} else {
|
|
||||||
newLen++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !flatten { // not just newLen == len, because we might have mergedBodies with single bodies inside
|
|
||||||
return mergedBodies(bodies)
|
|
||||||
}
|
|
||||||
|
|
||||||
if newLen == 0 {
|
|
||||||
// Don't allocate a new empty when we already have one
|
|
||||||
return emptyBody
|
|
||||||
}
|
|
||||||
|
|
||||||
new := make([]Body, 0, newLen)
|
|
||||||
for _, body := range bodies {
|
|
||||||
if children, merged := body.(mergedBodies); merged {
|
|
||||||
new = append(new, children...)
|
|
||||||
} else {
|
|
||||||
new = append(new, body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mergedBodies(new)
|
|
||||||
}
|
|
||||||
|
|
||||||
var emptyBody = mergedBodies([]Body{})
|
|
||||||
|
|
||||||
// EmptyBody returns a body with no content. This body can be used as a
|
|
||||||
// placeholder when a body is required but no body content is available.
|
|
||||||
func EmptyBody() Body {
|
|
||||||
return emptyBody
|
|
||||||
}
|
|
||||||
|
|
||||||
type mergedBodies []Body
|
|
||||||
|
|
||||||
// Content returns the content produced by applying the given schema to all
|
|
||||||
// of the merged bodies and merging the result.
|
|
||||||
//
|
|
||||||
// Although required attributes _are_ supported, they should be used sparingly
|
|
||||||
// with merged bodies since in this case there is no contextual information
|
|
||||||
// with which to return good diagnostics. Applications working with merged
|
|
||||||
// bodies may wish to mark all attributes as optional and then check for
|
|
||||||
// required attributes afterwards, to produce better diagnostics.
|
|
||||||
func (mb mergedBodies) Content(schema *BodySchema) (*BodyContent, Diagnostics) {
|
|
||||||
// the returned body will always be empty in this case, because mergedContent
|
|
||||||
// will only ever call Content on the child bodies.
|
|
||||||
content, _, diags := mb.mergedContent(schema, false)
|
|
||||||
return content, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mb mergedBodies) PartialContent(schema *BodySchema) (*BodyContent, Body, Diagnostics) {
|
|
||||||
return mb.mergedContent(schema, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mb mergedBodies) JustAttributes() (Attributes, Diagnostics) {
|
|
||||||
attrs := make(map[string]*Attribute)
|
|
||||||
var diags Diagnostics
|
|
||||||
|
|
||||||
for _, body := range mb {
|
|
||||||
thisAttrs, thisDiags := body.JustAttributes()
|
|
||||||
|
|
||||||
if len(thisDiags) != 0 {
|
|
||||||
diags = append(diags, thisDiags...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if thisAttrs != nil {
|
|
||||||
for name, attr := range thisAttrs {
|
|
||||||
if existing := attrs[name]; existing != nil {
|
|
||||||
diags = diags.Append(&Diagnostic{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Duplicate argument",
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"Argument %q was already set at %s",
|
|
||||||
name, existing.NameRange.String(),
|
|
||||||
),
|
|
||||||
Subject: &attr.NameRange,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs[name] = attr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mb mergedBodies) MissingItemRange() Range {
|
|
||||||
if len(mb) == 0 {
|
|
||||||
// Nothing useful to return here, so we'll return some garbage.
|
|
||||||
return Range{
|
|
||||||
Filename: "<empty>",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// arbitrarily use the first body's missing item range
|
|
||||||
return mb[0].MissingItemRange()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mb mergedBodies) mergedContent(schema *BodySchema, partial bool) (*BodyContent, Body, Diagnostics) {
|
|
||||||
// We need to produce a new schema with none of the attributes marked as
|
|
||||||
// required, since _any one_ of our bodies can contribute an attribute value.
|
|
||||||
// We'll separately check that all required attributes are present at
|
|
||||||
// the end.
|
|
||||||
mergedSchema := &BodySchema{
|
|
||||||
Blocks: schema.Blocks,
|
|
||||||
}
|
|
||||||
for _, attrS := range schema.Attributes {
|
|
||||||
mergedAttrS := attrS
|
|
||||||
mergedAttrS.Required = false
|
|
||||||
mergedSchema.Attributes = append(mergedSchema.Attributes, mergedAttrS)
|
|
||||||
}
|
|
||||||
|
|
||||||
var mergedLeftovers []Body
|
|
||||||
content := &BodyContent{
|
|
||||||
Attributes: map[string]*Attribute{},
|
|
||||||
}
|
|
||||||
|
|
||||||
var diags Diagnostics
|
|
||||||
for _, body := range mb {
|
|
||||||
var thisContent *BodyContent
|
|
||||||
var thisLeftovers Body
|
|
||||||
var thisDiags Diagnostics
|
|
||||||
|
|
||||||
if partial {
|
|
||||||
thisContent, thisLeftovers, thisDiags = body.PartialContent(mergedSchema)
|
|
||||||
} else {
|
|
||||||
thisContent, thisDiags = body.Content(mergedSchema)
|
|
||||||
}
|
|
||||||
|
|
||||||
if thisLeftovers != nil {
|
|
||||||
mergedLeftovers = append(mergedLeftovers, thisLeftovers)
|
|
||||||
}
|
|
||||||
if len(thisDiags) != 0 {
|
|
||||||
diags = append(diags, thisDiags...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if thisContent.Attributes != nil {
|
|
||||||
for name, attr := range thisContent.Attributes {
|
|
||||||
if existing := content.Attributes[name]; existing != nil {
|
|
||||||
diags = diags.Append(&Diagnostic{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Duplicate argument",
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"Argument %q was already set at %s",
|
|
||||||
name, existing.NameRange.String(),
|
|
||||||
),
|
|
||||||
Subject: &attr.NameRange,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
content.Attributes[name] = attr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(thisContent.Blocks) != 0 {
|
|
||||||
content.Blocks = append(content.Blocks, thisContent.Blocks...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we check for required attributes.
|
|
||||||
for _, attrS := range schema.Attributes {
|
|
||||||
if !attrS.Required {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if content.Attributes[attrS.Name] == nil {
|
|
||||||
// We don't have any context here to produce a good diagnostic,
|
|
||||||
// which is why we warn in the Content docstring to minimize the
|
|
||||||
// use of required attributes on merged bodies.
|
|
||||||
diags = diags.Append(&Diagnostic{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Missing required argument",
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"The argument %q is required, but was not set.",
|
|
||||||
attrS.Name,
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
leftoverBody := MergeBodies(mergedLeftovers)
|
|
||||||
return content, leftoverBody, diags
|
|
||||||
}
|
|
|
@ -1,288 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
"github.com/zclconf/go-cty/cty/convert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Index is a helper function that performs the same operation as the index
|
|
||||||
// operator in the HCL expression language. That is, the result is the
|
|
||||||
// same as it would be for collection[key] in a configuration expression.
|
|
||||||
//
|
|
||||||
// This is exported so that applications can perform indexing in a manner
|
|
||||||
// consistent with how the language does it, including handling of null and
|
|
||||||
// unknown values, etc.
|
|
||||||
//
|
|
||||||
// Diagnostics are produced if the given combination of values is not valid.
|
|
||||||
// Therefore a pointer to a source range must be provided to use in diagnostics,
|
|
||||||
// though nil can be provided if the calling application is going to
|
|
||||||
// ignore the subject of the returned diagnostics anyway.
|
|
||||||
func Index(collection, key cty.Value, srcRange *Range) (cty.Value, Diagnostics) {
|
|
||||||
if collection.IsNull() {
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Attempt to index null value",
|
|
||||||
Detail: "This value is null, so it does not have any indices.",
|
|
||||||
Subject: srcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if key.IsNull() {
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Invalid index",
|
|
||||||
Detail: "Can't use a null value as an indexing key.",
|
|
||||||
Subject: srcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ty := collection.Type()
|
|
||||||
kty := key.Type()
|
|
||||||
if kty == cty.DynamicPseudoType || ty == cty.DynamicPseudoType {
|
|
||||||
return cty.DynamicVal, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
|
|
||||||
case ty.IsListType() || ty.IsTupleType() || ty.IsMapType():
|
|
||||||
var wantType cty.Type
|
|
||||||
switch {
|
|
||||||
case ty.IsListType() || ty.IsTupleType():
|
|
||||||
wantType = cty.Number
|
|
||||||
case ty.IsMapType():
|
|
||||||
wantType = cty.String
|
|
||||||
default:
|
|
||||||
// should never happen
|
|
||||||
panic("don't know what key type we want")
|
|
||||||
}
|
|
||||||
|
|
||||||
key, keyErr := convert.Convert(key, wantType)
|
|
||||||
if keyErr != nil {
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Invalid index",
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"The given key does not identify an element in this collection value: %s.",
|
|
||||||
keyErr.Error(),
|
|
||||||
),
|
|
||||||
Subject: srcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
has := collection.HasIndex(key)
|
|
||||||
if !has.IsKnown() {
|
|
||||||
if ty.IsTupleType() {
|
|
||||||
return cty.DynamicVal, nil
|
|
||||||
} else {
|
|
||||||
return cty.UnknownVal(ty.ElementType()), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if has.False() {
|
|
||||||
// We have a more specialized error message for the situation of
|
|
||||||
// using a fractional number to index into a sequence, because
|
|
||||||
// that will tend to happen if the user is trying to use division
|
|
||||||
// to calculate an index and not realizing that HCL does float
|
|
||||||
// division rather than integer division.
|
|
||||||
if (ty.IsListType() || ty.IsTupleType()) && key.Type().Equals(cty.Number) {
|
|
||||||
if key.IsKnown() && !key.IsNull() {
|
|
||||||
bf := key.AsBigFloat()
|
|
||||||
if _, acc := bf.Int(nil); acc != big.Exact {
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Invalid index",
|
|
||||||
Detail: fmt.Sprintf("The given key does not identify an element in this collection value: indexing a sequence requires a whole number, but the given index (%g) has a fractional part.", bf),
|
|
||||||
Subject: srcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Invalid index",
|
|
||||||
Detail: "The given key does not identify an element in this collection value.",
|
|
||||||
Subject: srcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return collection.Index(key), nil
|
|
||||||
|
|
||||||
case ty.IsObjectType():
|
|
||||||
key, keyErr := convert.Convert(key, cty.String)
|
|
||||||
if keyErr != nil {
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Invalid index",
|
|
||||||
Detail: fmt.Sprintf(
|
|
||||||
"The given key does not identify an element in this collection value: %s.",
|
|
||||||
keyErr.Error(),
|
|
||||||
),
|
|
||||||
Subject: srcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !collection.IsKnown() {
|
|
||||||
return cty.DynamicVal, nil
|
|
||||||
}
|
|
||||||
if !key.IsKnown() {
|
|
||||||
return cty.DynamicVal, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
attrName := key.AsString()
|
|
||||||
|
|
||||||
if !ty.HasAttribute(attrName) {
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Invalid index",
|
|
||||||
Detail: "The given key does not identify an element in this collection value.",
|
|
||||||
Subject: srcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return collection.GetAttr(attrName), nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Invalid index",
|
|
||||||
Detail: "This value does not have any indices.",
|
|
||||||
Subject: srcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAttr is a helper function that performs the same operation as the
|
|
||||||
// attribute access in the HCL expression language. That is, the result is the
|
|
||||||
// same as it would be for obj.attr in a configuration expression.
|
|
||||||
//
|
|
||||||
// This is exported so that applications can access attributes in a manner
|
|
||||||
// consistent with how the language does it, including handling of null and
|
|
||||||
// unknown values, etc.
|
|
||||||
//
|
|
||||||
// Diagnostics are produced if the given combination of values is not valid.
|
|
||||||
// Therefore a pointer to a source range must be provided to use in diagnostics,
|
|
||||||
// though nil can be provided if the calling application is going to
|
|
||||||
// ignore the subject of the returned diagnostics anyway.
|
|
||||||
func GetAttr(obj cty.Value, attrName string, srcRange *Range) (cty.Value, Diagnostics) {
|
|
||||||
if obj.IsNull() {
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Attempt to get attribute from null value",
|
|
||||||
Detail: "This value is null, so it does not have any attributes.",
|
|
||||||
Subject: srcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ty := obj.Type()
|
|
||||||
switch {
|
|
||||||
case ty.IsObjectType():
|
|
||||||
if !ty.HasAttribute(attrName) {
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Unsupported attribute",
|
|
||||||
Detail: fmt.Sprintf("This object does not have an attribute named %q.", attrName),
|
|
||||||
Subject: srcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !obj.IsKnown() {
|
|
||||||
return cty.UnknownVal(ty.AttributeType(attrName)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj.GetAttr(attrName), nil
|
|
||||||
case ty.IsMapType():
|
|
||||||
if !obj.IsKnown() {
|
|
||||||
return cty.UnknownVal(ty.ElementType()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
idx := cty.StringVal(attrName)
|
|
||||||
if obj.HasIndex(idx).False() {
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Missing map element",
|
|
||||||
Detail: fmt.Sprintf("This map does not have an element with the key %q.", attrName),
|
|
||||||
Subject: srcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj.Index(idx), nil
|
|
||||||
case ty == cty.DynamicPseudoType:
|
|
||||||
return cty.DynamicVal, nil
|
|
||||||
default:
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Unsupported attribute",
|
|
||||||
Detail: "This value does not have any attributes.",
|
|
||||||
Subject: srcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApplyPath is a helper function that applies a cty.Path to a value using the
|
|
||||||
// indexing and attribute access operations from HCL.
|
|
||||||
//
|
|
||||||
// This is similar to calling the path's own Apply method, but ApplyPath uses
|
|
||||||
// the more relaxed typing rules that apply to these operations in HCL, rather
|
|
||||||
// than cty's relatively-strict rules. ApplyPath is implemented in terms of
|
|
||||||
// Index and GetAttr, and so it has the same behavior for individual steps
|
|
||||||
// but will stop and return any errors returned by intermediate steps.
|
|
||||||
//
|
|
||||||
// Diagnostics are produced if the given path cannot be applied to the given
|
|
||||||
// value. Therefore a pointer to a source range must be provided to use in
|
|
||||||
// diagnostics, though nil can be provided if the calling application is going
|
|
||||||
// to ignore the subject of the returned diagnostics anyway.
|
|
||||||
func ApplyPath(val cty.Value, path cty.Path, srcRange *Range) (cty.Value, Diagnostics) {
|
|
||||||
var diags Diagnostics
|
|
||||||
|
|
||||||
for _, step := range path {
|
|
||||||
var stepDiags Diagnostics
|
|
||||||
switch ts := step.(type) {
|
|
||||||
case cty.IndexStep:
|
|
||||||
val, stepDiags = Index(val, ts.Key, srcRange)
|
|
||||||
case cty.GetAttrStep:
|
|
||||||
val, stepDiags = GetAttr(val, ts.Name, srcRange)
|
|
||||||
default:
|
|
||||||
// Should never happen because the above are all of the step types.
|
|
||||||
diags = diags.Append(&Diagnostic{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Invalid path step",
|
|
||||||
Detail: fmt.Sprintf("Go type %T is not a valid path step. This is a bug in this program.", step),
|
|
||||||
Subject: srcRange,
|
|
||||||
})
|
|
||||||
return cty.DynamicVal, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
diags = append(diags, stepDiags...)
|
|
||||||
if stepDiags.HasErrors() {
|
|
||||||
return cty.DynamicVal, diags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return val, diags
|
|
||||||
}
|
|
|
@ -1,275 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// Pos represents a single position in a source file, by addressing the
|
|
||||||
// start byte of a unicode character encoded in UTF-8.
|
|
||||||
//
|
|
||||||
// Pos is generally used only in the context of a Range, which then defines
|
|
||||||
// which source file the position is within.
|
|
||||||
type Pos struct {
|
|
||||||
// Line is the source code line where this position points. Lines are
|
|
||||||
// counted starting at 1 and incremented for each newline character
|
|
||||||
// encountered.
|
|
||||||
Line int
|
|
||||||
|
|
||||||
// Column is the source code column where this position points, in
|
|
||||||
// unicode characters, with counting starting at 1.
|
|
||||||
//
|
|
||||||
// Column counts characters as they appear visually, so for example a
|
|
||||||
// latin letter with a combining diacritic mark counts as one character.
|
|
||||||
// This is intended for rendering visual markers against source code in
|
|
||||||
// contexts where these diacritics would be rendered in a single character
|
|
||||||
// cell. Technically speaking, Column is counting grapheme clusters as
|
|
||||||
// used in unicode normalization.
|
|
||||||
Column int
|
|
||||||
|
|
||||||
// Byte is the byte offset into the file where the indicated character
|
|
||||||
// begins. This is a zero-based offset to the first byte of the first
|
|
||||||
// UTF-8 codepoint sequence in the character, and thus gives a position
|
|
||||||
// that can be resolved _without_ awareness of Unicode characters.
|
|
||||||
Byte int
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitialPos is a suitable position to use to mark the start of a file.
|
|
||||||
var InitialPos = Pos{Byte: 0, Line: 1, Column: 1}
|
|
||||||
|
|
||||||
// Range represents a span of characters between two positions in a source
|
|
||||||
// file.
|
|
||||||
//
|
|
||||||
// This struct is usually used by value in types that represent AST nodes,
|
|
||||||
// but by pointer in types that refer to the positions of other objects,
|
|
||||||
// such as in diagnostics.
|
|
||||||
type Range struct {
|
|
||||||
// Filename is the name of the file into which this range's positions
|
|
||||||
// point.
|
|
||||||
Filename string
|
|
||||||
|
|
||||||
// Start and End represent the bounds of this range. Start is inclusive
|
|
||||||
// and End is exclusive.
|
|
||||||
Start, End Pos
|
|
||||||
}
|
|
||||||
|
|
||||||
// RangeBetween returns a new range that spans from the beginning of the
|
|
||||||
// start range to the end of the end range.
|
|
||||||
//
|
|
||||||
// The result is meaningless if the two ranges do not belong to the same
|
|
||||||
// source file or if the end range appears before the start range.
|
|
||||||
func RangeBetween(start, end Range) Range {
|
|
||||||
return Range{
|
|
||||||
Filename: start.Filename,
|
|
||||||
Start: start.Start,
|
|
||||||
End: end.End,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RangeOver returns a new range that covers both of the given ranges and
|
|
||||||
// possibly additional content between them if the two ranges do not overlap.
|
|
||||||
//
|
|
||||||
// If either range is empty then it is ignored. The result is empty if both
|
|
||||||
// given ranges are empty.
|
|
||||||
//
|
|
||||||
// The result is meaningless if the two ranges to not belong to the same
|
|
||||||
// source file.
|
|
||||||
func RangeOver(a, b Range) Range {
|
|
||||||
if a.Empty() {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
if b.Empty() {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
var start, end Pos
|
|
||||||
if a.Start.Byte < b.Start.Byte {
|
|
||||||
start = a.Start
|
|
||||||
} else {
|
|
||||||
start = b.Start
|
|
||||||
}
|
|
||||||
if a.End.Byte > b.End.Byte {
|
|
||||||
end = a.End
|
|
||||||
} else {
|
|
||||||
end = b.End
|
|
||||||
}
|
|
||||||
return Range{
|
|
||||||
Filename: a.Filename,
|
|
||||||
Start: start,
|
|
||||||
End: end,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainsPos returns true if and only if the given position is contained within
|
|
||||||
// the receiving range.
|
|
||||||
//
|
|
||||||
// In the unlikely case that the line/column information disagree with the byte
|
|
||||||
// offset information in the given position or receiving range, the byte
|
|
||||||
// offsets are given priority.
|
|
||||||
func (r Range) ContainsPos(pos Pos) bool {
|
|
||||||
return r.ContainsOffset(pos.Byte)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainsOffset returns true if and only if the given byte offset is within
|
|
||||||
// the receiving Range.
|
|
||||||
func (r Range) ContainsOffset(offset int) bool {
|
|
||||||
return offset >= r.Start.Byte && offset < r.End.Byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ptr returns a pointer to a copy of the receiver. This is a convenience when
|
|
||||||
// ranges in places where pointers are required, such as in Diagnostic, but
|
|
||||||
// the range in question is returned from a method. Go would otherwise not
|
|
||||||
// allow one to take the address of a function call.
|
|
||||||
func (r Range) Ptr() *Range {
|
|
||||||
return &r
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a compact string representation of the receiver.
|
|
||||||
// Callers should generally prefer to present a range more visually,
|
|
||||||
// e.g. via markers directly on the relevant portion of source code.
|
|
||||||
func (r Range) String() string {
|
|
||||||
if r.Start.Line == r.End.Line {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"%s:%d,%d-%d",
|
|
||||||
r.Filename,
|
|
||||||
r.Start.Line, r.Start.Column,
|
|
||||||
r.End.Column,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"%s:%d,%d-%d,%d",
|
|
||||||
r.Filename,
|
|
||||||
r.Start.Line, r.Start.Column,
|
|
||||||
r.End.Line, r.End.Column,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Range) Empty() bool {
|
|
||||||
return r.Start.Byte == r.End.Byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanSliceBytes returns true if SliceBytes could return an accurate
|
|
||||||
// sub-slice of the given slice.
|
|
||||||
//
|
|
||||||
// This effectively tests whether the start and end offsets of the range
|
|
||||||
// are within the bounds of the slice, and thus whether SliceBytes can be
|
|
||||||
// trusted to produce an accurate start and end position within that slice.
|
|
||||||
func (r Range) CanSliceBytes(b []byte) bool {
|
|
||||||
switch {
|
|
||||||
case r.Start.Byte < 0 || r.Start.Byte > len(b):
|
|
||||||
return false
|
|
||||||
case r.End.Byte < 0 || r.End.Byte > len(b):
|
|
||||||
return false
|
|
||||||
case r.End.Byte < r.Start.Byte:
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SliceBytes returns a sub-slice of the given slice that is covered by the
|
|
||||||
// receiving range, assuming that the given slice is the source code of the
|
|
||||||
// file indicated by r.Filename.
|
|
||||||
//
|
|
||||||
// If the receiver refers to any byte offsets that are outside of the slice
|
|
||||||
// then the result is constrained to the overlapping portion only, to avoid
|
|
||||||
// a panic. Use CanSliceBytes to determine if the result is guaranteed to
|
|
||||||
// be an accurate span of the requested range.
|
|
||||||
func (r Range) SliceBytes(b []byte) []byte {
|
|
||||||
start := r.Start.Byte
|
|
||||||
end := r.End.Byte
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
} else if start > len(b) {
|
|
||||||
start = len(b)
|
|
||||||
}
|
|
||||||
if end < 0 {
|
|
||||||
end = 0
|
|
||||||
} else if end > len(b) {
|
|
||||||
end = len(b)
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
return b[start:end]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overlaps returns true if the receiver and the other given range share any
|
|
||||||
// characters in common.
|
|
||||||
func (r Range) Overlaps(other Range) bool {
|
|
||||||
switch {
|
|
||||||
case r.Filename != other.Filename:
|
|
||||||
// If the ranges are in different files then they can't possibly overlap
|
|
||||||
return false
|
|
||||||
case r.Empty() || other.Empty():
|
|
||||||
// Empty ranges can never overlap
|
|
||||||
return false
|
|
||||||
case r.ContainsOffset(other.Start.Byte) || r.ContainsOffset(other.End.Byte):
|
|
||||||
return true
|
|
||||||
case other.ContainsOffset(r.Start.Byte) || other.ContainsOffset(r.End.Byte):
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overlap finds a range that is either identical to or a sub-range of both
|
|
||||||
// the receiver and the other given range. It returns an empty range
|
|
||||||
// within the receiver if there is no overlap between the two ranges.
|
|
||||||
//
|
|
||||||
// A non-empty result is either identical to or a subset of the receiver.
|
|
||||||
func (r Range) Overlap(other Range) Range {
|
|
||||||
if !r.Overlaps(other) {
|
|
||||||
// Start == End indicates an empty range
|
|
||||||
return Range{
|
|
||||||
Filename: r.Filename,
|
|
||||||
Start: r.Start,
|
|
||||||
End: r.Start,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var start, end Pos
|
|
||||||
if r.Start.Byte > other.Start.Byte {
|
|
||||||
start = r.Start
|
|
||||||
} else {
|
|
||||||
start = other.Start
|
|
||||||
}
|
|
||||||
if r.End.Byte < other.End.Byte {
|
|
||||||
end = r.End
|
|
||||||
} else {
|
|
||||||
end = other.End
|
|
||||||
}
|
|
||||||
|
|
||||||
return Range{
|
|
||||||
Filename: r.Filename,
|
|
||||||
Start: start,
|
|
||||||
End: end,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PartitionAround finds the portion of the given range that overlaps with
|
|
||||||
// the reciever and returns three ranges: the portion of the reciever that
|
|
||||||
// precedes the overlap, the overlap itself, and then the portion of the
|
|
||||||
// reciever that comes after the overlap.
|
|
||||||
//
|
|
||||||
// If the two ranges do not overlap then all three returned ranges are empty.
|
|
||||||
//
|
|
||||||
// If the given range aligns with or extends beyond either extent of the
|
|
||||||
// reciever then the corresponding outer range will be empty.
|
|
||||||
func (r Range) PartitionAround(other Range) (before, overlap, after Range) {
|
|
||||||
overlap = r.Overlap(other)
|
|
||||||
if overlap.Empty() {
|
|
||||||
return overlap, overlap, overlap
|
|
||||||
}
|
|
||||||
|
|
||||||
before = Range{
|
|
||||||
Filename: r.Filename,
|
|
||||||
Start: r.Start,
|
|
||||||
End: overlap.Start,
|
|
||||||
}
|
|
||||||
after = Range{
|
|
||||||
Filename: r.Filename,
|
|
||||||
Start: overlap.End,
|
|
||||||
End: r.End,
|
|
||||||
}
|
|
||||||
|
|
||||||
return before, overlap, after
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
|
|
||||||
"github.com/apparentlymart/go-textseg/textseg"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RangeScanner is a helper that will scan over a buffer using a bufio.SplitFunc
|
|
||||||
// and visit a source range for each token matched.
|
|
||||||
//
|
|
||||||
// For example, this can be used with bufio.ScanLines to find the source range
|
|
||||||
// for each line in the file, skipping over the actual newline characters, which
|
|
||||||
// may be useful when printing source code snippets as part of diagnostic
|
|
||||||
// messages.
|
|
||||||
//
|
|
||||||
// The line and column information in the returned ranges is produced by
|
|
||||||
// counting newline characters and grapheme clusters respectively, which
|
|
||||||
// mimics the behavior we expect from a parser when producing ranges.
|
|
||||||
type RangeScanner struct {
|
|
||||||
filename string
|
|
||||||
b []byte
|
|
||||||
cb bufio.SplitFunc
|
|
||||||
|
|
||||||
pos Pos // position of next byte to process in b
|
|
||||||
cur Range // latest range
|
|
||||||
tok []byte // slice of b that is covered by cur
|
|
||||||
err error // error from last scan, if any
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRangeScanner creates a new RangeScanner for the given buffer, producing
|
|
||||||
// ranges for the given filename.
|
|
||||||
//
|
|
||||||
// Since ranges have grapheme-cluster granularity rather than byte granularity,
|
|
||||||
// the scanner will produce incorrect results if the given SplitFunc creates
|
|
||||||
// tokens between grapheme cluster boundaries. In particular, it is incorrect
|
|
||||||
// to use RangeScanner with bufio.ScanRunes because it will produce tokens
|
|
||||||
// around individual UTF-8 sequences, which will split any multi-sequence
|
|
||||||
// grapheme clusters.
|
|
||||||
func NewRangeScanner(b []byte, filename string, cb bufio.SplitFunc) *RangeScanner {
|
|
||||||
return NewRangeScannerFragment(b, filename, InitialPos, cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRangeScannerFragment is like NewRangeScanner but the ranges it produces
|
|
||||||
// will be offset by the given starting position, which is appropriate for
|
|
||||||
// sub-slices of a file, whereas NewRangeScanner assumes it is scanning an
|
|
||||||
// entire file.
|
|
||||||
func NewRangeScannerFragment(b []byte, filename string, start Pos, cb bufio.SplitFunc) *RangeScanner {
|
|
||||||
return &RangeScanner{
|
|
||||||
filename: filename,
|
|
||||||
b: b,
|
|
||||||
cb: cb,
|
|
||||||
pos: start,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sc *RangeScanner) Scan() bool {
|
|
||||||
if sc.pos.Byte >= len(sc.b) || sc.err != nil {
|
|
||||||
// All done
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we're operating on an in-memory buffer, we always pass the whole
|
|
||||||
// remainder of the buffer to our SplitFunc and set isEOF to let it know
|
|
||||||
// that it has the whole thing.
|
|
||||||
advance, token, err := sc.cb(sc.b[sc.pos.Byte:], true)
|
|
||||||
|
|
||||||
// Since we are setting isEOF to true this should never happen, but
|
|
||||||
// if it does we will just abort and assume the SplitFunc is misbehaving.
|
|
||||||
if advance == 0 && token == nil && err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
sc.err = err
|
|
||||||
sc.cur = Range{
|
|
||||||
Filename: sc.filename,
|
|
||||||
Start: sc.pos,
|
|
||||||
End: sc.pos,
|
|
||||||
}
|
|
||||||
sc.tok = nil
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
sc.tok = token
|
|
||||||
start := sc.pos
|
|
||||||
end := sc.pos
|
|
||||||
new := sc.pos
|
|
||||||
|
|
||||||
// adv is similar to token but it also includes any subsequent characters
|
|
||||||
// we're being asked to skip over by the SplitFunc.
|
|
||||||
// adv is a slice covering any additional bytes we are skipping over, based
|
|
||||||
// on what the SplitFunc told us to do with advance.
|
|
||||||
adv := sc.b[sc.pos.Byte : sc.pos.Byte+advance]
|
|
||||||
|
|
||||||
// We now need to scan over our token to count the grapheme clusters
|
|
||||||
// so we can correctly advance Column, and count the newlines so we
|
|
||||||
// can correctly advance Line.
|
|
||||||
advR := bytes.NewReader(adv)
|
|
||||||
gsc := bufio.NewScanner(advR)
|
|
||||||
advanced := 0
|
|
||||||
gsc.Split(textseg.ScanGraphemeClusters)
|
|
||||||
for gsc.Scan() {
|
|
||||||
gr := gsc.Bytes()
|
|
||||||
new.Byte += len(gr)
|
|
||||||
new.Column++
|
|
||||||
|
|
||||||
// We rely here on the fact that \r\n is considered a grapheme cluster
|
|
||||||
// and so we don't need to worry about miscounting additional lines
|
|
||||||
// on files with Windows-style line endings.
|
|
||||||
if len(gr) != 0 && (gr[0] == '\r' || gr[0] == '\n') {
|
|
||||||
new.Column = 1
|
|
||||||
new.Line++
|
|
||||||
}
|
|
||||||
|
|
||||||
if advanced < len(token) {
|
|
||||||
// If we've not yet found the end of our token then we'll
|
|
||||||
// also push our "end" marker along.
|
|
||||||
// (if advance > len(token) then we'll stop moving "end" early
|
|
||||||
// so that the caller only sees the range covered by token.)
|
|
||||||
end = new
|
|
||||||
}
|
|
||||||
advanced += len(gr)
|
|
||||||
}
|
|
||||||
|
|
||||||
sc.cur = Range{
|
|
||||||
Filename: sc.filename,
|
|
||||||
Start: start,
|
|
||||||
End: end,
|
|
||||||
}
|
|
||||||
sc.pos = new
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Range returns a range that covers the latest token obtained after a call
|
|
||||||
// to Scan returns true.
|
|
||||||
func (sc *RangeScanner) Range() Range {
|
|
||||||
return sc.cur
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns the slice of the input buffer that is covered by the range
|
|
||||||
// that would be returned by Range.
|
|
||||||
func (sc *RangeScanner) Bytes() []byte {
|
|
||||||
return sc.tok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Err can be called after Scan returns false to determine if the latest read
|
|
||||||
// resulted in an error, and obtain that error if so.
|
|
||||||
func (sc *RangeScanner) Err() error {
|
|
||||||
return sc.err
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
// BlockHeaderSchema represents the shape of a block header, and is
|
|
||||||
// used for matching blocks within bodies.
|
|
||||||
type BlockHeaderSchema struct {
|
|
||||||
Type string
|
|
||||||
LabelNames []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// AttributeSchema represents the requirements for an attribute, and is used
|
|
||||||
// for matching attributes within bodies.
|
|
||||||
type AttributeSchema struct {
|
|
||||||
Name string
|
|
||||||
Required bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// BodySchema represents the desired shallow structure of a body.
|
|
||||||
type BodySchema struct {
|
|
||||||
Attributes []AttributeSchema
|
|
||||||
Blocks []BlockHeaderSchema
|
|
||||||
}
|
|
|
@ -1,691 +0,0 @@
|
||||||
# HCL Syntax-Agnostic Information Model
|
|
||||||
|
|
||||||
This is the specification for the general information model (abstract types and
|
|
||||||
semantics) for hcl. HCL is a system for defining configuration languages for
|
|
||||||
applications. The HCL information model is designed to support multiple
|
|
||||||
concrete syntaxes for configuration, each with a mapping to the model defined
|
|
||||||
in this specification.
|
|
||||||
|
|
||||||
The two primary syntaxes intended for use in conjunction with this model are
|
|
||||||
[the HCL native syntax](./hclsyntax/spec.md) and [the JSON syntax](./json/spec.md).
|
|
||||||
In principle other syntaxes are possible as long as either their language model
|
|
||||||
is sufficiently rich to express the concepts described in this specification
|
|
||||||
or the language targets a well-defined subset of the specification.
|
|
||||||
|
|
||||||
## Structural Elements
|
|
||||||
|
|
||||||
The primary structural element is the _body_, which is a container representing
|
|
||||||
a set of zero or more _attributes_ and a set of zero or more _blocks_.
|
|
||||||
|
|
||||||
A _configuration file_ is the top-level object, and will usually be produced
|
|
||||||
by reading a file from disk and parsing it as a particular syntax. A
|
|
||||||
configuration file has its own _body_, representing the top-level attributes
|
|
||||||
and blocks.
|
|
||||||
|
|
||||||
An _attribute_ is a name and value pair associated with a body. Attribute names
|
|
||||||
are unique within a given body. Attribute values are provided as _expressions_,
|
|
||||||
which are discussed in detail in a later section.
|
|
||||||
|
|
||||||
A _block_ is a nested structure that has a _type name_, zero or more string
|
|
||||||
_labels_ (e.g. identifiers), and a nested body.
|
|
||||||
|
|
||||||
Together the structural elements create a hierarchical data structure, with
|
|
||||||
attributes intended to represent the direct properties of a particular object
|
|
||||||
in the calling application, and blocks intended to represent child objects
|
|
||||||
of a particular object.
|
|
||||||
|
|
||||||
## Body Content
|
|
||||||
|
|
||||||
To support the expression of the HCL concepts in languages whose information
|
|
||||||
model is a subset of HCL's, such as JSON, a _body_ is an opaque container
|
|
||||||
whose content can only be accessed by providing information on the expected
|
|
||||||
structure of the content.
|
|
||||||
|
|
||||||
The specification for each syntax must describe how its physical constructs
|
|
||||||
are mapped on to body content given a schema. For syntaxes that have
|
|
||||||
first-class syntax distinguishing attributes and bodies this can be relatively
|
|
||||||
straightforward, while more detailed mapping rules may be required in syntaxes
|
|
||||||
where the representation of attributes vs. blocks is ambiguous.
|
|
||||||
|
|
||||||
### Schema-driven Processing
|
|
||||||
|
|
||||||
Schema-driven processing is the primary way to access body content.
|
|
||||||
A _body schema_ is a description of what is expected within a particular body,
|
|
||||||
which can then be used to extract the _body content_, which then provides
|
|
||||||
access to the specific attributes and blocks requested.
|
|
||||||
|
|
||||||
A _body schema_ consists of a list of _attribute schemata_ and
|
|
||||||
_block header schemata_:
|
|
||||||
|
|
||||||
- An _attribute schema_ provides the name of an attribute and whether its
|
|
||||||
presence is required.
|
|
||||||
|
|
||||||
- A _block header schema_ provides a block type name and the semantic names
|
|
||||||
assigned to each of the labels of that block type, if any.
|
|
||||||
|
|
||||||
Within a schema, it is an error to request the same attribute name twice or
|
|
||||||
to request a block type whose name is also an attribute name. While this can
|
|
||||||
in principle be supported in some syntaxes, in other syntaxes the attribute
|
|
||||||
and block namespaces are combined and so an attribute cannot coexist with
|
|
||||||
a block whose type name is identical to the attribute name.
|
|
||||||
|
|
||||||
The result of applying a body schema to a body is _body content_, which
|
|
||||||
consists of an _attribute map_ and a _block sequence_:
|
|
||||||
|
|
||||||
- The _attribute map_ is a map data structure whose keys are attribute names
|
|
||||||
and whose values are _expressions_ that represent the corresponding attribute
|
|
||||||
values.
|
|
||||||
|
|
||||||
- The _block sequence_ is an ordered sequence of blocks, with each specifying
|
|
||||||
a block _type name_, the sequence of _labels_ specified for the block,
|
|
||||||
and the body object (not body _content_) representing the block's own body.
|
|
||||||
|
|
||||||
After obtaining _body content_, the calling application may continue processing
|
|
||||||
by evaluating attribute expressions and/or recursively applying further
|
|
||||||
schema-driven processing to the child block bodies.
|
|
||||||
|
|
||||||
**Note:** The _body schema_ is intentionally minimal, to reduce the set of
|
|
||||||
mapping rules that must be defined for each syntax. Higher-level utility
|
|
||||||
libraries may be provided to assist in the construction of a schema and
|
|
||||||
perform additional processing, such as automatically evaluating attribute
|
|
||||||
expressions and assigning their result values into a data structure, or
|
|
||||||
recursively applying a schema to child blocks. Such utilities are not part of
|
|
||||||
this core specification and will vary depending on the capabilities and idiom
|
|
||||||
of the implementation language.
|
|
||||||
|
|
||||||
### _Dynamic Attributes_ Processing
|
|
||||||
|
|
||||||
The _schema-driven_ processing model is useful when the expected structure
|
|
||||||
of a body is known a priori by the calling application. Some blocks are
|
|
||||||
instead more free-form, such as a user-provided set of arbitrary key/value
|
|
||||||
pairs.
|
|
||||||
|
|
||||||
The alternative _dynamic attributes_ processing mode allows for this more
|
|
||||||
ad-hoc approach. Processing in this mode behaves as if a schema had been
|
|
||||||
constructed without any _block header schemata_ and with an attribute
|
|
||||||
schema for each distinct key provided within the physical representation
|
|
||||||
of the body.
|
|
||||||
|
|
||||||
The means by which _distinct keys_ are identified is dependent on the
|
|
||||||
physical syntax; this processing mode assumes that the syntax has a way
|
|
||||||
to enumerate keys provided by the author and identify expressions that
|
|
||||||
correspond with those keys, but does not define the means by which this is
|
|
||||||
done.
|
|
||||||
|
|
||||||
The result of _dynamic attributes_ processing is an _attribute map_ as
|
|
||||||
defined in the previous section. No _block sequence_ is produced in this
|
|
||||||
processing mode.
|
|
||||||
|
|
||||||
### Partial Processing of Body Content
|
|
||||||
|
|
||||||
Under _schema-driven processing_, by default the given schema is assumed
|
|
||||||
to be exhaustive, such that any attribute or block not matched by schema
|
|
||||||
elements is considered an error. This allows feedback about unsupported
|
|
||||||
attributes and blocks (such as typos) to be provided.
|
|
||||||
|
|
||||||
An alternative is _partial processing_, where any additional elements within
|
|
||||||
the body are not considered an error.
|
|
||||||
|
|
||||||
Under partial processing, the result is both body content as described
|
|
||||||
above _and_ a new body that represents any body elements that remain after
|
|
||||||
the schema has been processed.
|
|
||||||
|
|
||||||
Specifically:
|
|
||||||
|
|
||||||
- Any attribute whose name is specified in the schema is returned in body
|
|
||||||
content and elided from the new body.
|
|
||||||
|
|
||||||
- Any block whose type is specified in the schema is returned in body content
|
|
||||||
and elided from the new body.
|
|
||||||
|
|
||||||
- Any attribute or block _not_ meeting the above conditions is placed into
|
|
||||||
the new body, unmodified.
|
|
||||||
|
|
||||||
The new body can then be recursively processed using any of the body
|
|
||||||
processing models. This facility allows different subsets of body content
|
|
||||||
to be processed by different parts of the calling application.
|
|
||||||
|
|
||||||
Processing a body in two steps — first partial processing of a source body,
|
|
||||||
then exhaustive processing of the returned body — is equivalent to single-step
|
|
||||||
processing with a schema that is the union of the schemata used
|
|
||||||
across the two steps.
|
|
||||||
|
|
||||||
## Expressions
|
|
||||||
|
|
||||||
Attribute values are represented by _expressions_. Depending on the concrete
|
|
||||||
syntax in use, an expression may just be a literal value or it may describe
|
|
||||||
a computation in terms of literal values, variables, and functions.
|
|
||||||
|
|
||||||
Each syntax defines its own representation of expressions. For syntaxes based
|
|
||||||
in languages that do not have any non-literal expression syntax, it is
|
|
||||||
recommended to embed the template language from
|
|
||||||
[the native syntax](./hclsyntax/spec.md) e.g. as a post-processing step on
|
|
||||||
string literals.
|
|
||||||
|
|
||||||
### Expression Evaluation
|
|
||||||
|
|
||||||
In order to obtain a concrete value, each expression must be _evaluated_.
|
|
||||||
Evaluation is performed in terms of an evaluation context, which
|
|
||||||
consists of the following:
|
|
||||||
|
|
||||||
- An _evaluation mode_, which is defined below.
|
|
||||||
- A _variable scope_, which provides a set of named variables for use in
|
|
||||||
expressions.
|
|
||||||
- A _function table_, which provides a set of named functions for use in
|
|
||||||
expressions.
|
|
||||||
|
|
||||||
The _evaluation mode_ allows for two different interpretations of an
|
|
||||||
expression:
|
|
||||||
|
|
||||||
- In _literal-only mode_, variables and functions are not available and it
|
|
||||||
is assumed that the calling application's intent is to treat the attribute
|
|
||||||
value as a literal.
|
|
||||||
|
|
||||||
- In _full expression mode_, variables and functions are defined and it is
|
|
||||||
assumed that the calling application wishes to provide a full expression
|
|
||||||
language for definition of the attribute value.
|
|
||||||
|
|
||||||
The actual behavior of these two modes depends on the syntax in use. For
|
|
||||||
languages with first-class expression syntax, these two modes may be considered
|
|
||||||
equivalent, with _literal-only mode_ simply not defining any variables or
|
|
||||||
functions. For languages that embed arbitrary expressions via string templates,
|
|
||||||
_literal-only mode_ may disable such processing, allowing literal strings to
|
|
||||||
pass through without interpretation as templates.
|
|
||||||
|
|
||||||
Since literal-only mode does not support variables and functions, it is an
|
|
||||||
error for the calling application to enable this mode and yet provide a
|
|
||||||
variable scope and/or function table.
|
|
||||||
|
|
||||||
## Values and Value Types
|
|
||||||
|
|
||||||
The result of expression evaluation is a _value_. Each value has a _type_,
|
|
||||||
which is dynamically determined during evaluation. The _variable scope_ in
|
|
||||||
the evaluation context is a map from variable name to value, using the same
|
|
||||||
definition of value.
|
|
||||||
|
|
||||||
The type system for HCL values is intended to be of a level abstraction
|
|
||||||
suitable for configuration of various applications. A well-defined,
|
|
||||||
implementation-language-agnostic type system is defined to allow for
|
|
||||||
consistent processing of configuration across many implementation languages.
|
|
||||||
Concrete implementations may provide additional functionality to lower
|
|
||||||
HCL values and types to corresponding native language types, which may then
|
|
||||||
impose additional constraints on the values outside of the scope of this
|
|
||||||
specification.
|
|
||||||
|
|
||||||
Two values are _equal_ if and only if they have identical types and their
|
|
||||||
values are equal according to the rules of their shared type.
|
|
||||||
|
|
||||||
### Primitive Types
|
|
||||||
|
|
||||||
The primitive types are _string_, _bool_, and _number_.
|
|
||||||
|
|
||||||
A _string_ is a sequence of unicode characters. Two strings are equal if
|
|
||||||
NFC normalization ([UAX#15](http://unicode.org/reports/tr15/)
|
|
||||||
of each string produces two identical sequences of characters.
|
|
||||||
NFC normalization ensures that, for example, a precomposed combination of a
|
|
||||||
latin letter and a diacritic compares equal with the letter followed by
|
|
||||||
a combining diacritic.
|
|
||||||
|
|
||||||
The _bool_ type has only two non-null values: _true_ and _false_. Two bool
|
|
||||||
values are equal if and only if they are either both true or both false.
|
|
||||||
|
|
||||||
A _number_ is an arbitrary-precision floating point value. An implementation
|
|
||||||
_must_ make the full-precision values available to the calling application
|
|
||||||
for interpretation into any suitable number representation. An implementation
|
|
||||||
may in practice implement numbers with limited precision so long as the
|
|
||||||
following constraints are met:
|
|
||||||
|
|
||||||
- Integers are represented with at least 256 bits.
|
|
||||||
- Non-integer numbers are represented as floating point values with a
|
|
||||||
mantissa of at least 256 bits and a signed binary exponent of at least
|
|
||||||
16 bits.
|
|
||||||
- An error is produced if an integer value given in source cannot be
|
|
||||||
represented precisely.
|
|
||||||
- An error is produced if a non-integer value cannot be represented due to
|
|
||||||
overflow.
|
|
||||||
- A non-integer number is rounded to the nearest possible value when a
|
|
||||||
value is of too high a precision to be represented.
|
|
||||||
|
|
||||||
The _number_ type also requires representation of both positive and negative
|
|
||||||
infinity. A "not a number" (NaN) value is _not_ provided nor used.
|
|
||||||
|
|
||||||
Two number values are equal if they are numerically equal to the precision
|
|
||||||
associated with the number. Positive infinity and negative infinity are
|
|
||||||
equal to themselves but not to each other. Positive infinity is greater than
|
|
||||||
any other number value, and negative infinity is less than any other number
|
|
||||||
value.
|
|
||||||
|
|
||||||
Some syntaxes may be unable to represent numeric literals of arbitrary
|
|
||||||
precision. This must be defined in the syntax specification as part of its
|
|
||||||
description of mapping numeric literals to HCL values.
|
|
||||||
|
|
||||||
### Structural Types
|
|
||||||
|
|
||||||
_Structural types_ are types that are constructed by combining other types.
|
|
||||||
Each distinct combination of other types is itself a distinct type. There
|
|
||||||
are two structural type _kinds_:
|
|
||||||
|
|
||||||
- _Object types_ are constructed of a set of named attributes, each of which
|
|
||||||
has a type. Attribute names are always strings. (_Object_ attributes are a
|
|
||||||
distinct idea from _body_ attributes, though calling applications
|
|
||||||
may choose to blur the distinction by use of common naming schemes.)
|
|
||||||
- _Tuple types_ are constructed of a sequence of elements, each of which
|
|
||||||
has a type.
|
|
||||||
|
|
||||||
Values of structural types are compared for equality in terms of their
|
|
||||||
attributes or elements. A structural type value is equal to another if and
|
|
||||||
only if all of the corresponding attributes or elements are equal.
|
|
||||||
|
|
||||||
Two structural types are identical if they are of the same kind and
|
|
||||||
have attributes or elements with identical types.
|
|
||||||
|
|
||||||
### Collection Types
|
|
||||||
|
|
||||||
_Collection types_ are types that combine together an arbitrary number of
|
|
||||||
values of some other single type. There are three collection type _kinds_:
|
|
||||||
|
|
||||||
- _List types_ represent ordered sequences of values of their element type.
|
|
||||||
- _Map types_ represent values of their element type accessed via string keys.
|
|
||||||
- _Set types_ represent unordered sets of distinct values of their element type.
|
|
||||||
|
|
||||||
For each of these kinds and each distinct element type there is a distinct
|
|
||||||
collection type. For example, "list of string" is a distinct type from
|
|
||||||
"set of string", and "list of number" is a distinct type from "list of string".
|
|
||||||
|
|
||||||
Values of collection types are compared for equality in terms of their
|
|
||||||
elements. A collection type value is equal to another if and only if both
|
|
||||||
have the same number of elements and their corresponding elements are equal.
|
|
||||||
|
|
||||||
Two collection types are identical if they are of the same kind and have
|
|
||||||
the same element type.
|
|
||||||
|
|
||||||
### Null values
|
|
||||||
|
|
||||||
Each type has a null value. The null value of a type represents the absence
|
|
||||||
of a value, but with type information retained to allow for type checking.
|
|
||||||
|
|
||||||
Null values are used primarily to represent the conditional absence of a
|
|
||||||
body attribute. In a syntax with a conditional operator, one of the result
|
|
||||||
values of that conditional may be null to indicate that the attribute should be
|
|
||||||
considered not present in that case.
|
|
||||||
|
|
||||||
Calling applications _should_ consider an attribute with a null value as
|
|
||||||
equivalent to the value not being present at all.
|
|
||||||
|
|
||||||
A null value of a particular type is equal to itself.
|
|
||||||
|
|
||||||
### Unknown Values and the Dynamic Pseudo-type
|
|
||||||
|
|
||||||
An _unknown value_ is a placeholder for a value that is not yet known.
|
|
||||||
Operations on unknown values themselves return unknown values that have a
|
|
||||||
type appropriate to the operation. For example, adding together two unknown
|
|
||||||
numbers yields an unknown number, while comparing two unknown values of any
|
|
||||||
type for equality yields an unknown bool.
|
|
||||||
|
|
||||||
Each type has a distinct unknown value. For example, an unknown _number_ is
|
|
||||||
a distinct value from an unknown _string_.
|
|
||||||
|
|
||||||
_The dynamic pseudo-type_ is a placeholder for a type that is not yet known.
|
|
||||||
The only values of this type are its null value and its unknown value. It is
|
|
||||||
referred to as a _pseudo-type_ because it should not be considered a type in
|
|
||||||
its own right, but rather as a placeholder for a type yet to be established.
|
|
||||||
The unknown value of the dynamic pseudo-type is referred to as _the dynamic
|
|
||||||
value_.
|
|
||||||
|
|
||||||
Operations on values of the dynamic pseudo-type behave as if it is a value
|
|
||||||
of the expected type, optimistically assuming that once the value and type
|
|
||||||
are known they will be valid for the operation. For example, adding together
|
|
||||||
a number and the dynamic value produces an unknown number.
|
|
||||||
|
|
||||||
Unknown values and the dynamic pseudo-type can be used as a mechanism for
|
|
||||||
partial type checking and semantic checking: by evaluating an expression with
|
|
||||||
all variables set to an unknown value, the expression can be evaluated to
|
|
||||||
produce an unknown value of a given type, or produce an error if any operation
|
|
||||||
is provably invalid with only type information.
|
|
||||||
|
|
||||||
Unknown values and the dynamic pseudo-type must never be returned from
|
|
||||||
operations unless at least one operand is unknown or dynamic. Calling
|
|
||||||
applications are guaranteed that unless the global scope includes unknown
|
|
||||||
values, or the function table includes functions that return unknown values,
|
|
||||||
no expression will evaluate to an unknown value. The calling application is
|
|
||||||
thus in total control over the use and meaning of unknown values.
|
|
||||||
|
|
||||||
The dynamic pseudo-type is identical only to itself.
|
|
||||||
|
|
||||||
### Capsule Types
|
|
||||||
|
|
||||||
A _capsule type_ is a custom type defined by the calling application. A value
|
|
||||||
of a capsule type is considered opaque to HCL, but may be accepted
|
|
||||||
by functions provided by the calling application.
|
|
||||||
|
|
||||||
A particular capsule type is identical only to itself. The equality of two
|
|
||||||
values of the same capsule type is defined by the calling application. No
|
|
||||||
other operations are supported for values of capsule types.
|
|
||||||
|
|
||||||
Support for capsule types in a HCL implementation is optional. Capsule types
|
|
||||||
are intended to allow calling applications to pass through values that are
|
|
||||||
not part of the standard type system. For example, an application that
|
|
||||||
deals with raw binary data may define a capsule type representing a byte
|
|
||||||
array, and provide functions that produce or operate on byte arrays.
|
|
||||||
|
|
||||||
### Type Specifications
|
|
||||||
|
|
||||||
In certain situations it is necessary to define expectations about the expected
|
|
||||||
type of a value. Whereas two _types_ have a commutative _identity_ relationship,
|
|
||||||
a type has a non-commutative _matches_ relationship with a _type specification_.
|
|
||||||
A type specification is, in practice, just a different interpretation of a
|
|
||||||
type such that:
|
|
||||||
|
|
||||||
- Any type _matches_ any type that it is identical to.
|
|
||||||
|
|
||||||
- Any type _matches_ the dynamic pseudo-type.
|
|
||||||
|
|
||||||
For example, given a type specification "list of dynamic pseudo-type", the
|
|
||||||
concrete types "list of string" and "list of map" match, but the
|
|
||||||
type "set of string" does not.
|
|
||||||
|
|
||||||
## Functions and Function Calls
|
|
||||||
|
|
||||||
The evaluation context used to evaluate an expression includes a function
|
|
||||||
table, which represents an application-defined set of named functions
|
|
||||||
available for use in expressions.
|
|
||||||
|
|
||||||
Each syntax defines whether function calls are supported and how they are
|
|
||||||
physically represented in source code, but the semantics of function calls are
|
|
||||||
defined here to ensure consistent results across syntaxes and to allow
|
|
||||||
applications to provide functions that are interoperable with all syntaxes.
|
|
||||||
|
|
||||||
A _function_ is defined from the following elements:
|
|
||||||
|
|
||||||
- Zero or more _positional parameters_, each with a name used for documentation,
|
|
||||||
a type specification for expected argument values, and a flag for whether
|
|
||||||
each of null values, unknown values, and values of the dynamic pseudo-type
|
|
||||||
are accepted.
|
|
||||||
|
|
||||||
- Zero or one _variadic parameters_, with the same structure as the _positional_
|
|
||||||
parameters, which if present collects any additional arguments provided at
|
|
||||||
the function call site.
|
|
||||||
|
|
||||||
- A _result type definition_, which specifies the value type returned for each
|
|
||||||
valid sequence of argument values.
|
|
||||||
|
|
||||||
- A _result value definition_, which specifies the value returned for each
|
|
||||||
valid sequence of argument values.
|
|
||||||
|
|
||||||
A _function call_, regardless of source syntax, consists of a sequence of
|
|
||||||
argument values. The argument values are each mapped to a corresponding
|
|
||||||
parameter as follows:
|
|
||||||
|
|
||||||
- For each of the function's positional parameters in sequence, take the next
|
|
||||||
argument. If there are no more arguments, the call is erroneous.
|
|
||||||
|
|
||||||
- If the function has a variadic parameter, take all remaining arguments that
|
|
||||||
where not yet assigned to a positional parameter and collect them into
|
|
||||||
a sequence of variadic arguments that each correspond to the variadic
|
|
||||||
parameter.
|
|
||||||
|
|
||||||
- If the function has _no_ variadic parameter, it is an error if any arguments
|
|
||||||
remain after taking one argument for each positional parameter.
|
|
||||||
|
|
||||||
After mapping each argument to a parameter, semantic checking proceeds
|
|
||||||
for each argument:
|
|
||||||
|
|
||||||
- If the argument value corresponding to a parameter does not match the
|
|
||||||
parameter's type specification, the call is erroneous.
|
|
||||||
|
|
||||||
- If the argument value corresponding to a parameter is null and the parameter
|
|
||||||
is not specified as accepting nulls, the call is erroneous.
|
|
||||||
|
|
||||||
- If the argument value corresponding to a parameter is the dynamic value
|
|
||||||
and the parameter is not specified as accepting values of the dynamic
|
|
||||||
pseudo-type, the call is valid but its _result type_ is forced to be the
|
|
||||||
dynamic pseudo type.
|
|
||||||
|
|
||||||
- If neither of the above conditions holds for any argument, the call is
|
|
||||||
valid and the function's value type definition is used to determine the
|
|
||||||
call's _result type_. A function _may_ vary its result type depending on
|
|
||||||
the argument _values_ as well as the argument _types_; for example, a
|
|
||||||
function that decodes a JSON value will return a different result type
|
|
||||||
depending on the data structure described by the given JSON source code.
|
|
||||||
|
|
||||||
If semantic checking succeeds without error, the call is _executed_:
|
|
||||||
|
|
||||||
- For each argument, if its value is unknown and its corresponding parameter
|
|
||||||
is not specified as accepting unknowns, the _result value_ is forced to be an
|
|
||||||
unknown value of the result type.
|
|
||||||
|
|
||||||
- If the previous condition does not apply, the function's result value
|
|
||||||
definition is used to determine the call's _result value_.
|
|
||||||
|
|
||||||
The result of a function call expression is either an error, if one of the
|
|
||||||
erroneous conditions above applies, or the _result value_.
|
|
||||||
|
|
||||||
## Type Conversions and Unification
|
|
||||||
|
|
||||||
Values given in configuration may not always match the expectations of the
|
|
||||||
operations applied to them or to the calling application. In such situations,
|
|
||||||
automatic type conversion is attempted as a convenience to the user.
|
|
||||||
|
|
||||||
Along with conversions to a _specified_ type, it is sometimes necessary to
|
|
||||||
ensure that a selection of values are all of the _same_ type, without any
|
|
||||||
constraint on which type that is. This is the process of _type unification_,
|
|
||||||
which attempts to find the most general type that all of the given types can
|
|
||||||
be converted to.
|
|
||||||
|
|
||||||
Both type conversions and unification are defined in the syntax-agnostic
|
|
||||||
model to ensure consistency of behavior between syntaxes.
|
|
||||||
|
|
||||||
Type conversions are broadly characterized into two categories: _safe_ and
|
|
||||||
_unsafe_. A conversion is "safe" if any distinct value of the source type
|
|
||||||
has a corresponding distinct value in the target type. A conversion is
|
|
||||||
"unsafe" if either the target type values are _not_ distinct (information
|
|
||||||
may be lost in conversion) or if some values of the source type do not have
|
|
||||||
any corresponding value in the target type. An unsafe conversion may result
|
|
||||||
in an error.
|
|
||||||
|
|
||||||
A given type can always be converted to itself, which is a no-op.
|
|
||||||
|
|
||||||
### Conversion of Null Values
|
|
||||||
|
|
||||||
All null values are safely convertable to a null value of any other type,
|
|
||||||
regardless of other type-specific rules specified in the sections below.
|
|
||||||
|
|
||||||
### Conversion to and from the Dynamic Pseudo-type
|
|
||||||
|
|
||||||
Conversion _from_ the dynamic pseudo-type _to_ any other type always succeeds,
|
|
||||||
producing an unknown value of the target type.
|
|
||||||
|
|
||||||
Conversion of any value _to_ the dynamic pseudo-type is a no-op. The result
|
|
||||||
is the input value, verbatim. This is the only situation where the conversion
|
|
||||||
result value is not of the given target type.
|
|
||||||
|
|
||||||
### Primitive Type Conversions
|
|
||||||
|
|
||||||
Bidirectional conversions are available between the string and number types,
|
|
||||||
and between the string and boolean types.
|
|
||||||
|
|
||||||
The bool value true corresponds to the string containing the characters "true",
|
|
||||||
while the bool value false corresponds to the string containing the characters
|
|
||||||
"false". Conversion from bool to string is safe, while the converse is
|
|
||||||
unsafe. The strings "1" and "0" are alternative string representations
|
|
||||||
of true and false respectively. It is an error to convert a string other than
|
|
||||||
the four in this paragraph to type bool.
|
|
||||||
|
|
||||||
A number value is converted to string by translating its integer portion
|
|
||||||
into a sequence of decimal digits (`0` through `9`), and then if it has a
|
|
||||||
non-zero fractional part, a period `.` followed by a sequence of decimal
|
|
||||||
digits representing its fractional part. No exponent portion is included.
|
|
||||||
The number is converted at its full precision. Conversion from number to
|
|
||||||
string is safe.
|
|
||||||
|
|
||||||
A string is converted to a number value by reversing the above mapping.
|
|
||||||
No exponent portion is allowed. Conversion from string to number is unsafe.
|
|
||||||
It is an error to convert a string that does not comply with the expected
|
|
||||||
syntax to type number.
|
|
||||||
|
|
||||||
No direct conversion is available between the bool and number types.
|
|
||||||
|
|
||||||
### Collection and Structural Type Conversions
|
|
||||||
|
|
||||||
Conversion from set types to list types is _safe_, as long as their
|
|
||||||
element types are safely convertable. If the element types are _unsafely_
|
|
||||||
convertable, then the collection conversion is also unsafe. Each set element
|
|
||||||
becomes a corresponding list element, in an undefined order. Although no
|
|
||||||
particular ordering is required, implementations _should_ produce list
|
|
||||||
elements in a consistent order for a given input set, as a convenience
|
|
||||||
to calling applications.
|
|
||||||
|
|
||||||
Conversion from list types to set types is _unsafe_, as long as their element
|
|
||||||
types are convertable. Each distinct list item becomes a distinct set item.
|
|
||||||
If two list items are equal, one of the two is lost in the conversion.
|
|
||||||
|
|
||||||
Conversion from tuple types to list types permitted if all of the
|
|
||||||
tuple element types are convertable to the target list element type.
|
|
||||||
The safety of the conversion depends on the safety of each of the element
|
|
||||||
conversions. Each element in turn is converted to the list element type,
|
|
||||||
producing a list of identical length.
|
|
||||||
|
|
||||||
Conversion from tuple types to set types is permitted, behaving as if the
|
|
||||||
tuple type was first converted to a list of the same element type and then
|
|
||||||
that list converted to the target set type.
|
|
||||||
|
|
||||||
Conversion from object types to map types is permitted if all of the object
|
|
||||||
attribute types are convertable to the target map element type. The safety
|
|
||||||
of the conversion depends on the safety of each of the attribute conversions.
|
|
||||||
Each attribute in turn is converted to the map element type, and map element
|
|
||||||
keys are set to the name of each corresponding object attribute.
|
|
||||||
|
|
||||||
Conversion from list and set types to tuple types is permitted, following
|
|
||||||
the opposite steps as the converse conversions. Such conversions are _unsafe_.
|
|
||||||
It is an error to convert a list or set to a tuple type whose number of
|
|
||||||
elements does not match the list or set length.
|
|
||||||
|
|
||||||
Conversion from map types to object types is permitted if each map key
|
|
||||||
corresponds to an attribute in the target object type. It is an error to
|
|
||||||
convert from a map value whose set of keys does not exactly match the target
|
|
||||||
type's attributes. The conversion takes the opposite steps of the converse
|
|
||||||
conversion.
|
|
||||||
|
|
||||||
Conversion from one object type to another is permitted as long as the
|
|
||||||
common attribute names have convertable types. Any attribute present in the
|
|
||||||
target type but not in the source type is populated with a null value of
|
|
||||||
the appropriate type.
|
|
||||||
|
|
||||||
Conversion from one tuple type to another is permitted as long as the
|
|
||||||
tuples have the same length and the elements have convertable types.
|
|
||||||
|
|
||||||
### Type Unification
|
|
||||||
|
|
||||||
Type unification is an operation that takes a list of types and attempts
|
|
||||||
to find a single type to which they can all be converted. Since some
|
|
||||||
type pairs have bidirectional conversions, preference is given to _safe_
|
|
||||||
conversions. In technical terms, all possible types are arranged into
|
|
||||||
a lattice, from which a most general supertype is selected where possible.
|
|
||||||
|
|
||||||
The type resulting from type unification may be one of the input types, or
|
|
||||||
it may be an entirely new type produced by combination of two or more
|
|
||||||
input types.
|
|
||||||
|
|
||||||
The following rules do not guarantee a valid result. In addition to these
|
|
||||||
rules, unification fails if any of the given types are not convertable
|
|
||||||
(per the above rules) to the selected result type.
|
|
||||||
|
|
||||||
The following unification rules apply transitively. That is, if a rule is
|
|
||||||
defined from A to B, and one from B to C, then A can unify to C.
|
|
||||||
|
|
||||||
Number and bool types both unify with string by preferring string.
|
|
||||||
|
|
||||||
Two collection types of the same kind unify according to the unification
|
|
||||||
of their element types.
|
|
||||||
|
|
||||||
List and set types unify by preferring the list type.
|
|
||||||
|
|
||||||
Map and object types unify by preferring the object type.
|
|
||||||
|
|
||||||
List, set and tuple types unify by preferring the tuple type.
|
|
||||||
|
|
||||||
The dynamic pseudo-type unifies with any other type by selecting that other
|
|
||||||
type. The dynamic pseudo-type is the result type only if _all_ input types
|
|
||||||
are the dynamic pseudo-type.
|
|
||||||
|
|
||||||
Two object types unify by constructing a new type whose attributes are
|
|
||||||
the union of those of the two input types. Any common attributes themselves
|
|
||||||
have their types unified.
|
|
||||||
|
|
||||||
Two tuple types of the same length unify constructing a new type of the
|
|
||||||
same length whose elements are the unification of the corresponding elements
|
|
||||||
in the two input types.
|
|
||||||
|
|
||||||
## Static Analysis
|
|
||||||
|
|
||||||
In most applications, full expression evaluation is sufficient for understanding
|
|
||||||
the provided configuration. However, some specialized applications require more
|
|
||||||
direct access to the physical structures in the expressions, which can for
|
|
||||||
example allow the construction of new language constructs in terms of the
|
|
||||||
existing syntax elements.
|
|
||||||
|
|
||||||
Since static analysis analyses the physical structure of configuration, the
|
|
||||||
details will vary depending on syntax. Each syntax must decide which of its
|
|
||||||
physical structures corresponds to the following analyses, producing error
|
|
||||||
diagnostics if they are applied to inappropriate expressions.
|
|
||||||
|
|
||||||
The following are the required static analysis functions:
|
|
||||||
|
|
||||||
- **Static List**: Require list/tuple construction syntax to be used and
|
|
||||||
return a list of expressions for each of the elements given.
|
|
||||||
|
|
||||||
- **Static Map**: Require map/object construction syntax to be used and
|
|
||||||
return a list of key/value pairs -- both expressions -- for each of
|
|
||||||
the elements given. The usual constraint that a map key must be a string
|
|
||||||
must not apply to this analysis, thus allowing applications to interpret
|
|
||||||
arbitrary keys as they see fit.
|
|
||||||
|
|
||||||
- **Static Call**: Require function call syntax to be used and return an
|
|
||||||
object describing the called function name and a list of expressions
|
|
||||||
representing each of the call arguments.
|
|
||||||
|
|
||||||
- **Static Traversal**: Require a reference to a symbol in the variable
|
|
||||||
scope and return a description of the path from the root scope to the
|
|
||||||
accessed attribute or index.
|
|
||||||
|
|
||||||
The intent of a calling application using these features is to require a more
|
|
||||||
rigid interpretation of the configuration than in expression evaluation.
|
|
||||||
Syntax implementations should make use of the extra contextual information
|
|
||||||
provided in order to make an intuitive mapping onto the constructs of the
|
|
||||||
underlying syntax, possibly interpreting the expression slightly differently
|
|
||||||
than it would be interpreted in normal evaluation.
|
|
||||||
|
|
||||||
Each syntax must define which of its expression elements each of the analyses
|
|
||||||
above applies to, and how those analyses behave given those expression elements.
|
|
||||||
|
|
||||||
## Implementation Considerations
|
|
||||||
|
|
||||||
Implementations of this specification are free to adopt any strategy that
|
|
||||||
produces behavior consistent with the specification. This non-normative
|
|
||||||
section describes some possible implementation strategies that are consistent
|
|
||||||
with the goals of this specification.
|
|
||||||
|
|
||||||
### Language-agnosticism
|
|
||||||
|
|
||||||
The language-agnosticism of this specification assumes that certain behaviors
|
|
||||||
are implemented separately for each syntax:
|
|
||||||
|
|
||||||
- Matching of a body schema with the physical elements of a body in the
|
|
||||||
source language, to determine correspondence between physical constructs
|
|
||||||
and schema elements.
|
|
||||||
|
|
||||||
- Implementing the _dynamic attributes_ body processing mode by either
|
|
||||||
interpreting all physical constructs as attributes or producing an error
|
|
||||||
if non-attribute constructs are present.
|
|
||||||
|
|
||||||
- Providing an evaluation function for all possible expressions that produces
|
|
||||||
a value given an evaluation context.
|
|
||||||
|
|
||||||
- Providing the static analysis functionality described above in a manner that
|
|
||||||
makes sense within the convention of the syntax.
|
|
||||||
|
|
||||||
The suggested implementation strategy is to use an implementation language's
|
|
||||||
closest concept to an _abstract type_, _virtual type_ or _interface type_
|
|
||||||
to represent both Body and Expression. Each language-specific implementation
|
|
||||||
can then provide an implementation of each of these types wrapping AST nodes
|
|
||||||
or other physical constructs from the language parser.
|
|
|
@ -1,40 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
|
||||||
|
|
||||||
type staticExpr struct {
|
|
||||||
val cty.Value
|
|
||||||
rng Range
|
|
||||||
}
|
|
||||||
|
|
||||||
// StaticExpr returns an Expression that always evaluates to the given value.
|
|
||||||
//
|
|
||||||
// This is useful to substitute default values for expressions that are
|
|
||||||
// not explicitly given in configuration and thus would otherwise have no
|
|
||||||
// Expression to return.
|
|
||||||
//
|
|
||||||
// Since expressions are expected to have a source range, the caller must
|
|
||||||
// provide one. Ideally this should be a real source range, but it can
|
|
||||||
// be a synthetic one (with an empty-string filename) if no suitable range
|
|
||||||
// is available.
|
|
||||||
func StaticExpr(val cty.Value, rng Range) Expression {
|
|
||||||
return staticExpr{val, rng}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e staticExpr) Value(ctx *EvalContext) (cty.Value, Diagnostics) {
|
|
||||||
return e.val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e staticExpr) Variables() []Traversal {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e staticExpr) Range() Range {
|
|
||||||
return e.rng
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e staticExpr) StartRange() Range {
|
|
||||||
return e.rng
|
|
||||||
}
|
|
|
@ -1,151 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
|
||||||
|
|
||||||
// File is the top-level node that results from parsing a HCL file.
|
|
||||||
type File struct {
|
|
||||||
Body Body
|
|
||||||
Bytes []byte
|
|
||||||
|
|
||||||
// Nav is used to integrate with the "hcled" editor integration package,
|
|
||||||
// and with diagnostic information formatters. It is not for direct use
|
|
||||||
// by a calling application.
|
|
||||||
Nav interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block represents a nested block within a Body.
|
|
||||||
type Block struct {
|
|
||||||
Type string
|
|
||||||
Labels []string
|
|
||||||
Body Body
|
|
||||||
|
|
||||||
DefRange Range // Range that can be considered the "definition" for seeking in an editor
|
|
||||||
TypeRange Range // Range for the block type declaration specifically.
|
|
||||||
LabelRanges []Range // Ranges for the label values specifically.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blocks is a sequence of Block.
|
|
||||||
type Blocks []*Block
|
|
||||||
|
|
||||||
// Attributes is a set of attributes keyed by their names.
|
|
||||||
type Attributes map[string]*Attribute
|
|
||||||
|
|
||||||
// Body is a container for attributes and blocks. It serves as the primary
|
|
||||||
// unit of hierarchical structure within configuration.
|
|
||||||
//
|
|
||||||
// The content of a body cannot be meaningfully interpreted without a schema,
|
|
||||||
// so Body represents the raw body content and has methods that allow the
|
|
||||||
// content to be extracted in terms of a given schema.
|
|
||||||
type Body interface {
|
|
||||||
// Content verifies that the entire body content conforms to the given
|
|
||||||
// schema and then returns it, and/or returns diagnostics. The returned
|
|
||||||
// body content is valid if non-nil, regardless of whether Diagnostics
|
|
||||||
// are provided, but diagnostics should still be eventually shown to
|
|
||||||
// the user.
|
|
||||||
Content(schema *BodySchema) (*BodyContent, Diagnostics)
|
|
||||||
|
|
||||||
// PartialContent is like Content except that it permits the configuration
|
|
||||||
// to contain additional blocks or attributes not specified in the
|
|
||||||
// schema. If any are present, the returned Body is non-nil and contains
|
|
||||||
// the remaining items from the body that were not selected by the schema.
|
|
||||||
PartialContent(schema *BodySchema) (*BodyContent, Body, Diagnostics)
|
|
||||||
|
|
||||||
// JustAttributes attempts to interpret all of the contents of the body
|
|
||||||
// as attributes, allowing for the contents to be accessed without a priori
|
|
||||||
// knowledge of the structure.
|
|
||||||
//
|
|
||||||
// The behavior of this method depends on the body's source language.
|
|
||||||
// Some languages, like JSON, can't distinguish between attributes and
|
|
||||||
// blocks without schema hints, but for languages that _can_ error
|
|
||||||
// diagnostics will be generated if any blocks are present in the body.
|
|
||||||
//
|
|
||||||
// Diagnostics may be produced for other reasons too, such as duplicate
|
|
||||||
// declarations of the same attribute.
|
|
||||||
JustAttributes() (Attributes, Diagnostics)
|
|
||||||
|
|
||||||
// MissingItemRange returns a range that represents where a missing item
|
|
||||||
// might hypothetically be inserted. This is used when producing
|
|
||||||
// diagnostics about missing required attributes or blocks. Not all bodies
|
|
||||||
// will have an obvious single insertion point, so the result here may
|
|
||||||
// be rather arbitrary.
|
|
||||||
MissingItemRange() Range
|
|
||||||
}
|
|
||||||
|
|
||||||
// BodyContent is the result of applying a BodySchema to a Body.
|
|
||||||
type BodyContent struct {
|
|
||||||
Attributes Attributes
|
|
||||||
Blocks Blocks
|
|
||||||
|
|
||||||
MissingItemRange Range
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attribute represents an attribute from within a body.
|
|
||||||
type Attribute struct {
|
|
||||||
Name string
|
|
||||||
Expr Expression
|
|
||||||
|
|
||||||
Range Range
|
|
||||||
NameRange Range
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expression is a literal value or an expression provided in the
|
|
||||||
// configuration, which can be evaluated within a scope to produce a value.
|
|
||||||
type Expression interface {
|
|
||||||
// Value returns the value resulting from evaluating the expression
|
|
||||||
// in the given evaluation context.
|
|
||||||
//
|
|
||||||
// The context may be nil, in which case the expression may contain
|
|
||||||
// only constants and diagnostics will be produced for any non-constant
|
|
||||||
// sub-expressions. (The exact definition of this depends on the source
|
|
||||||
// language.)
|
|
||||||
//
|
|
||||||
// The context may instead be set but have either its Variables or
|
|
||||||
// Functions maps set to nil, in which case only use of these features
|
|
||||||
// will return diagnostics.
|
|
||||||
//
|
|
||||||
// Different diagnostics are provided depending on whether the given
|
|
||||||
// context maps are nil or empty. In the former case, the message
|
|
||||||
// tells the user that variables/functions are not permitted at all,
|
|
||||||
// while in the latter case usage will produce a "not found" error for
|
|
||||||
// the specific symbol in question.
|
|
||||||
Value(ctx *EvalContext) (cty.Value, Diagnostics)
|
|
||||||
|
|
||||||
// Variables returns a list of variables referenced in the receiving
|
|
||||||
// expression. These are expressed as absolute Traversals, so may include
|
|
||||||
// additional information about how the variable is used, such as
|
|
||||||
// attribute lookups, which the calling application can potentially use
|
|
||||||
// to only selectively populate the scope.
|
|
||||||
Variables() []Traversal
|
|
||||||
|
|
||||||
Range() Range
|
|
||||||
StartRange() Range
|
|
||||||
}
|
|
||||||
|
|
||||||
// OfType filters the receiving block sequence by block type name,
|
|
||||||
// returning a new block sequence including only the blocks of the
|
|
||||||
// requested type.
|
|
||||||
func (els Blocks) OfType(typeName string) Blocks {
|
|
||||||
ret := make(Blocks, 0)
|
|
||||||
for _, el := range els {
|
|
||||||
if el.Type == typeName {
|
|
||||||
ret = append(ret, el)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByType transforms the receiving block sequence into a map from type
|
|
||||||
// name to block sequences of only that type.
|
|
||||||
func (els Blocks) ByType() map[string]Blocks {
|
|
||||||
ret := make(map[string]Blocks)
|
|
||||||
for _, el := range els {
|
|
||||||
ty := el.Type
|
|
||||||
if ret[ty] == nil {
|
|
||||||
ret[ty] = make(Blocks, 0, 1)
|
|
||||||
}
|
|
||||||
ret[ty] = append(ret[ty], el)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// The methods in this file all have the general pattern of making a best-effort
|
|
||||||
// to find one or more constructs that contain a given source position.
|
|
||||||
//
|
|
||||||
// These all operate by delegating to an optional method of the same name and
|
|
||||||
// signature on the file's root body, allowing each syntax to potentially
|
|
||||||
// provide its own implementations of these. For syntaxes that don't implement
|
|
||||||
// them, the result is always nil.
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// BlocksAtPos attempts to find all of the blocks that contain the given
|
|
||||||
// position, ordered so that the outermost block is first and the innermost
|
|
||||||
// block is last. This is a best-effort method that may not be able to produce
|
|
||||||
// a complete result for all positions or for all HCL syntaxes.
|
|
||||||
//
|
|
||||||
// If the returned slice is non-empty, the first element is guaranteed to
|
|
||||||
// represent the same block as would be the result of OutermostBlockAtPos and
|
|
||||||
// the last element the result of InnermostBlockAtPos. However, the
|
|
||||||
// implementation may return two different objects describing the same block,
|
|
||||||
// so comparison by pointer identity is not possible.
|
|
||||||
//
|
|
||||||
// The result is nil if no blocks at all contain the given position.
|
|
||||||
func (f *File) BlocksAtPos(pos Pos) []*Block {
|
|
||||||
// The root body of the file must implement this interface in order
|
|
||||||
// to support BlocksAtPos.
|
|
||||||
type Interface interface {
|
|
||||||
BlocksAtPos(pos Pos) []*Block
|
|
||||||
}
|
|
||||||
|
|
||||||
impl, ok := f.Body.(Interface)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return impl.BlocksAtPos(pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutermostBlockAtPos attempts to find a top-level block in the receiving file
|
|
||||||
// that contains the given position. This is a best-effort method that may not
|
|
||||||
// be able to produce a result for all positions or for all HCL syntaxes.
|
|
||||||
//
|
|
||||||
// The result is nil if no single block could be selected for any reason.
|
|
||||||
func (f *File) OutermostBlockAtPos(pos Pos) *Block {
|
|
||||||
// The root body of the file must implement this interface in order
|
|
||||||
// to support OutermostBlockAtPos.
|
|
||||||
type Interface interface {
|
|
||||||
OutermostBlockAtPos(pos Pos) *Block
|
|
||||||
}
|
|
||||||
|
|
||||||
impl, ok := f.Body.(Interface)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return impl.OutermostBlockAtPos(pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InnermostBlockAtPos attempts to find the most deeply-nested block in the
|
|
||||||
// receiving file that contains the given position. This is a best-effort
|
|
||||||
// method that may not be able to produce a result for all positions or for
|
|
||||||
// all HCL syntaxes.
|
|
||||||
//
|
|
||||||
// The result is nil if no single block could be selected for any reason.
|
|
||||||
func (f *File) InnermostBlockAtPos(pos Pos) *Block {
|
|
||||||
// The root body of the file must implement this interface in order
|
|
||||||
// to support InnermostBlockAtPos.
|
|
||||||
type Interface interface {
|
|
||||||
InnermostBlockAtPos(pos Pos) *Block
|
|
||||||
}
|
|
||||||
|
|
||||||
impl, ok := f.Body.(Interface)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return impl.InnermostBlockAtPos(pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutermostExprAtPos attempts to find an expression in the receiving file
|
|
||||||
// that contains the given position. This is a best-effort method that may not
|
|
||||||
// be able to produce a result for all positions or for all HCL syntaxes.
|
|
||||||
//
|
|
||||||
// Since expressions are often nested inside one another, this method returns
|
|
||||||
// the outermost "root" expression that is not contained by any other.
|
|
||||||
//
|
|
||||||
// The result is nil if no single expression could be selected for any reason.
|
|
||||||
func (f *File) OutermostExprAtPos(pos Pos) Expression {
|
|
||||||
// The root body of the file must implement this interface in order
|
|
||||||
// to support OutermostExprAtPos.
|
|
||||||
type Interface interface {
|
|
||||||
OutermostExprAtPos(pos Pos) Expression
|
|
||||||
}
|
|
||||||
|
|
||||||
impl, ok := f.Body.(Interface)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return impl.OutermostExprAtPos(pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AttributeAtPos attempts to find an attribute definition in the receiving
|
|
||||||
// file that contains the given position. This is a best-effort method that may
|
|
||||||
// not be able to produce a result for all positions or for all HCL syntaxes.
|
|
||||||
//
|
|
||||||
// The result is nil if no single attribute could be selected for any reason.
|
|
||||||
func (f *File) AttributeAtPos(pos Pos) *Attribute {
|
|
||||||
// The root body of the file must implement this interface in order
|
|
||||||
// to support OutermostExprAtPos.
|
|
||||||
type Interface interface {
|
|
||||||
AttributeAtPos(pos Pos) *Attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
impl, ok := f.Body.(Interface)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return impl.AttributeAtPos(pos)
|
|
||||||
}
|
|
|
@ -1,293 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Traversal is a description of traversing through a value through a
|
|
||||||
// series of operations such as attribute lookup, index lookup, etc.
|
|
||||||
//
|
|
||||||
// It is used to look up values in scopes, for example.
|
|
||||||
//
|
|
||||||
// The traversal operations are implementations of interface Traverser.
|
|
||||||
// This is a closed set of implementations, so the interface cannot be
|
|
||||||
// implemented from outside this package.
|
|
||||||
//
|
|
||||||
// A traversal can be absolute (its first value is a symbol name) or relative
|
|
||||||
// (starts from an existing value).
|
|
||||||
type Traversal []Traverser
|
|
||||||
|
|
||||||
// TraversalJoin appends a relative traversal to an absolute traversal to
|
|
||||||
// produce a new absolute traversal.
|
|
||||||
func TraversalJoin(abs Traversal, rel Traversal) Traversal {
|
|
||||||
if abs.IsRelative() {
|
|
||||||
panic("first argument to TraversalJoin must be absolute")
|
|
||||||
}
|
|
||||||
if !rel.IsRelative() {
|
|
||||||
panic("second argument to TraversalJoin must be relative")
|
|
||||||
}
|
|
||||||
|
|
||||||
ret := make(Traversal, len(abs)+len(rel))
|
|
||||||
copy(ret, abs)
|
|
||||||
copy(ret[len(abs):], rel)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// TraverseRel applies the receiving traversal to the given value, returning
|
|
||||||
// the resulting value. This is supported only for relative traversals,
|
|
||||||
// and will panic if applied to an absolute traversal.
|
|
||||||
func (t Traversal) TraverseRel(val cty.Value) (cty.Value, Diagnostics) {
|
|
||||||
if !t.IsRelative() {
|
|
||||||
panic("can't use TraverseRel on an absolute traversal")
|
|
||||||
}
|
|
||||||
|
|
||||||
current := val
|
|
||||||
var diags Diagnostics
|
|
||||||
for _, tr := range t {
|
|
||||||
var newDiags Diagnostics
|
|
||||||
current, newDiags = tr.TraversalStep(current)
|
|
||||||
diags = append(diags, newDiags...)
|
|
||||||
if newDiags.HasErrors() {
|
|
||||||
return cty.DynamicVal, diags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return current, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// TraverseAbs applies the receiving traversal to the given eval context,
|
|
||||||
// returning the resulting value. This is supported only for absolute
|
|
||||||
// traversals, and will panic if applied to a relative traversal.
|
|
||||||
func (t Traversal) TraverseAbs(ctx *EvalContext) (cty.Value, Diagnostics) {
|
|
||||||
if t.IsRelative() {
|
|
||||||
panic("can't use TraverseAbs on a relative traversal")
|
|
||||||
}
|
|
||||||
|
|
||||||
split := t.SimpleSplit()
|
|
||||||
root := split.Abs[0].(TraverseRoot)
|
|
||||||
name := root.Name
|
|
||||||
|
|
||||||
thisCtx := ctx
|
|
||||||
hasNonNil := false
|
|
||||||
for thisCtx != nil {
|
|
||||||
if thisCtx.Variables == nil {
|
|
||||||
thisCtx = thisCtx.parent
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
hasNonNil = true
|
|
||||||
val, exists := thisCtx.Variables[name]
|
|
||||||
if exists {
|
|
||||||
return split.Rel.TraverseRel(val)
|
|
||||||
}
|
|
||||||
thisCtx = thisCtx.parent
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasNonNil {
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Variables not allowed",
|
|
||||||
Detail: "Variables may not be used here.",
|
|
||||||
Subject: &root.SrcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestions := make([]string, 0, len(ctx.Variables))
|
|
||||||
thisCtx = ctx
|
|
||||||
for thisCtx != nil {
|
|
||||||
for k := range thisCtx.Variables {
|
|
||||||
suggestions = append(suggestions, k)
|
|
||||||
}
|
|
||||||
thisCtx = thisCtx.parent
|
|
||||||
}
|
|
||||||
suggestion := nameSuggestion(name, suggestions)
|
|
||||||
if suggestion != "" {
|
|
||||||
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cty.DynamicVal, Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Unknown variable",
|
|
||||||
Detail: fmt.Sprintf("There is no variable named %q.%s", name, suggestion),
|
|
||||||
Subject: &root.SrcRange,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRelative returns true if the receiver is a relative traversal, or false
|
|
||||||
// otherwise.
|
|
||||||
func (t Traversal) IsRelative() bool {
|
|
||||||
if len(t) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, firstIsRoot := t[0].(TraverseRoot); firstIsRoot {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimpleSplit returns a TraversalSplit where the name lookup is the absolute
|
|
||||||
// part and the remainder is the relative part. Supported only for
|
|
||||||
// absolute traversals, and will panic if applied to a relative traversal.
|
|
||||||
//
|
|
||||||
// This can be used by applications that have a relatively-simple variable
|
|
||||||
// namespace where only the top-level is directly populated in the scope, with
|
|
||||||
// everything else handled by relative lookups from those initial values.
|
|
||||||
func (t Traversal) SimpleSplit() TraversalSplit {
|
|
||||||
if t.IsRelative() {
|
|
||||||
panic("can't use SimpleSplit on a relative traversal")
|
|
||||||
}
|
|
||||||
return TraversalSplit{
|
|
||||||
Abs: t[0:1],
|
|
||||||
Rel: t[1:],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RootName returns the root name for a absolute traversal. Will panic if
|
|
||||||
// called on a relative traversal.
|
|
||||||
func (t Traversal) RootName() string {
|
|
||||||
if t.IsRelative() {
|
|
||||||
panic("can't use RootName on a relative traversal")
|
|
||||||
|
|
||||||
}
|
|
||||||
return t[0].(TraverseRoot).Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// SourceRange returns the source range for the traversal.
|
|
||||||
func (t Traversal) SourceRange() Range {
|
|
||||||
if len(t) == 0 {
|
|
||||||
// Nothing useful to return here, but we'll return something
|
|
||||||
// that's correctly-typed at least.
|
|
||||||
return Range{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return RangeBetween(t[0].SourceRange(), t[len(t)-1].SourceRange())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TraversalSplit represents a pair of traversals, the first of which is
|
|
||||||
// an absolute traversal and the second of which is relative to the first.
|
|
||||||
//
|
|
||||||
// This is used by calling applications that only populate prefixes of the
|
|
||||||
// traversals in the scope, with Abs representing the part coming from the
|
|
||||||
// scope and Rel representing the remaining steps once that part is
|
|
||||||
// retrieved.
|
|
||||||
type TraversalSplit struct {
|
|
||||||
Abs Traversal
|
|
||||||
Rel Traversal
|
|
||||||
}
|
|
||||||
|
|
||||||
// TraverseAbs traverses from a scope to the value resulting from the
|
|
||||||
// absolute traversal.
|
|
||||||
func (t TraversalSplit) TraverseAbs(ctx *EvalContext) (cty.Value, Diagnostics) {
|
|
||||||
return t.Abs.TraverseAbs(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TraverseRel traverses from a given value, assumed to be the result of
|
|
||||||
// TraverseAbs on some scope, to a final result for the entire split traversal.
|
|
||||||
func (t TraversalSplit) TraverseRel(val cty.Value) (cty.Value, Diagnostics) {
|
|
||||||
return t.Rel.TraverseRel(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traverse is a convenience function to apply TraverseAbs followed by
|
|
||||||
// TraverseRel.
|
|
||||||
func (t TraversalSplit) Traverse(ctx *EvalContext) (cty.Value, Diagnostics) {
|
|
||||||
v1, diags := t.TraverseAbs(ctx)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return cty.DynamicVal, diags
|
|
||||||
}
|
|
||||||
v2, newDiags := t.TraverseRel(v1)
|
|
||||||
diags = append(diags, newDiags...)
|
|
||||||
return v2, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join concatenates together the Abs and Rel parts to produce a single
|
|
||||||
// absolute traversal.
|
|
||||||
func (t TraversalSplit) Join() Traversal {
|
|
||||||
return TraversalJoin(t.Abs, t.Rel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RootName returns the root name for the absolute part of the split.
|
|
||||||
func (t TraversalSplit) RootName() string {
|
|
||||||
return t.Abs.RootName()
|
|
||||||
}
|
|
||||||
|
|
||||||
// A Traverser is a step within a Traversal.
|
|
||||||
type Traverser interface {
|
|
||||||
TraversalStep(cty.Value) (cty.Value, Diagnostics)
|
|
||||||
SourceRange() Range
|
|
||||||
isTraverserSigil() isTraverser
|
|
||||||
}
|
|
||||||
|
|
||||||
// Embed this in a struct to declare it as a Traverser
|
|
||||||
type isTraverser struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tr isTraverser) isTraverserSigil() isTraverser {
|
|
||||||
return isTraverser{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TraverseRoot looks up a root name in a scope. It is used as the first step
|
|
||||||
// of an absolute Traversal, and cannot itself be traversed directly.
|
|
||||||
type TraverseRoot struct {
|
|
||||||
isTraverser
|
|
||||||
Name string
|
|
||||||
SrcRange Range
|
|
||||||
}
|
|
||||||
|
|
||||||
// TraversalStep on a TraverseName immediately panics, because absolute
|
|
||||||
// traversals cannot be directly traversed.
|
|
||||||
func (tn TraverseRoot) TraversalStep(cty.Value) (cty.Value, Diagnostics) {
|
|
||||||
panic("Cannot traverse an absolute traversal")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tn TraverseRoot) SourceRange() Range {
|
|
||||||
return tn.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
// TraverseAttr looks up an attribute in its initial value.
|
|
||||||
type TraverseAttr struct {
|
|
||||||
isTraverser
|
|
||||||
Name string
|
|
||||||
SrcRange Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tn TraverseAttr) TraversalStep(val cty.Value) (cty.Value, Diagnostics) {
|
|
||||||
return GetAttr(val, tn.Name, &tn.SrcRange)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tn TraverseAttr) SourceRange() Range {
|
|
||||||
return tn.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
// TraverseIndex applies the index operation to its initial value.
|
|
||||||
type TraverseIndex struct {
|
|
||||||
isTraverser
|
|
||||||
Key cty.Value
|
|
||||||
SrcRange Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tn TraverseIndex) TraversalStep(val cty.Value) (cty.Value, Diagnostics) {
|
|
||||||
return Index(val, tn.Key, &tn.SrcRange)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tn TraverseIndex) SourceRange() Range {
|
|
||||||
return tn.SrcRange
|
|
||||||
}
|
|
||||||
|
|
||||||
// TraverseSplat applies the splat operation to its initial value.
|
|
||||||
type TraverseSplat struct {
|
|
||||||
isTraverser
|
|
||||||
Each Traversal
|
|
||||||
SrcRange Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tn TraverseSplat) TraversalStep(val cty.Value) (cty.Value, Diagnostics) {
|
|
||||||
panic("TraverseSplat not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tn TraverseSplat) SourceRange() Range {
|
|
||||||
return tn.SrcRange
|
|
||||||
}
|
|
|
@ -1,124 +0,0 @@
|
||||||
package hcl
|
|
||||||
|
|
||||||
// AbsTraversalForExpr attempts to interpret the given expression as
|
|
||||||
// an absolute traversal, or returns error diagnostic(s) if that is
|
|
||||||
// not possible for the given expression.
|
|
||||||
//
|
|
||||||
// A particular Expression implementation can support this function by
|
|
||||||
// offering a method called AsTraversal that takes no arguments and
|
|
||||||
// returns either a valid absolute traversal or nil to indicate that
|
|
||||||
// no traversal is possible. Alternatively, an implementation can support
|
|
||||||
// UnwrapExpression to delegate handling of this function to a wrapped
|
|
||||||
// Expression object.
|
|
||||||
//
|
|
||||||
// In most cases the calling application is interested in the value
|
|
||||||
// that results from an expression, but in rarer cases the application
|
|
||||||
// needs to see the the name of the variable and subsequent
|
|
||||||
// attributes/indexes itself, for example to allow users to give references
|
|
||||||
// to the variables themselves rather than to their values. An implementer
|
|
||||||
// of this function should at least support attribute and index steps.
|
|
||||||
func AbsTraversalForExpr(expr Expression) (Traversal, Diagnostics) {
|
|
||||||
type asTraversal interface {
|
|
||||||
AsTraversal() Traversal
|
|
||||||
}
|
|
||||||
|
|
||||||
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
|
|
||||||
_, supported := expr.(asTraversal)
|
|
||||||
return supported
|
|
||||||
})
|
|
||||||
|
|
||||||
if asT, supported := physExpr.(asTraversal); supported {
|
|
||||||
if traversal := asT.AsTraversal(); traversal != nil {
|
|
||||||
return traversal, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, Diagnostics{
|
|
||||||
&Diagnostic{
|
|
||||||
Severity: DiagError,
|
|
||||||
Summary: "Invalid expression",
|
|
||||||
Detail: "A single static variable reference is required: only attribute access and indexing with constant keys. No calculations, function calls, template expressions, etc are allowed here.",
|
|
||||||
Subject: expr.Range().Ptr(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RelTraversalForExpr is similar to AbsTraversalForExpr but it returns
|
|
||||||
// a relative traversal instead. Due to the nature of HCL expressions, the
|
|
||||||
// first element of the returned traversal is always a TraverseAttr, and
|
|
||||||
// then it will be followed by zero or more other expressions.
|
|
||||||
//
|
|
||||||
// Any expression accepted by AbsTraversalForExpr is also accepted by
|
|
||||||
// RelTraversalForExpr.
|
|
||||||
func RelTraversalForExpr(expr Expression) (Traversal, Diagnostics) {
|
|
||||||
traversal, diags := AbsTraversalForExpr(expr)
|
|
||||||
if len(traversal) > 0 {
|
|
||||||
ret := make(Traversal, len(traversal))
|
|
||||||
copy(ret, traversal)
|
|
||||||
root := traversal[0].(TraverseRoot)
|
|
||||||
ret[0] = TraverseAttr{
|
|
||||||
Name: root.Name,
|
|
||||||
SrcRange: root.SrcRange,
|
|
||||||
}
|
|
||||||
return ret, diags
|
|
||||||
}
|
|
||||||
return traversal, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExprAsKeyword attempts to interpret the given expression as a static keyword,
|
|
||||||
// returning the keyword string if possible, and the empty string if not.
|
|
||||||
//
|
|
||||||
// A static keyword, for the sake of this function, is a single identifier.
|
|
||||||
// For example, the following attribute has an expression that would produce
|
|
||||||
// the keyword "foo":
|
|
||||||
//
|
|
||||||
// example = foo
|
|
||||||
//
|
|
||||||
// This function is a variant of AbsTraversalForExpr, which uses the same
|
|
||||||
// interface on the given expression. This helper constrains the result
|
|
||||||
// further by requiring only a single root identifier.
|
|
||||||
//
|
|
||||||
// This function is intended to be used with the following idiom, to recognize
|
|
||||||
// situations where one of a fixed set of keywords is required and arbitrary
|
|
||||||
// expressions are not allowed:
|
|
||||||
//
|
|
||||||
// switch hcl.ExprAsKeyword(expr) {
|
|
||||||
// case "allow":
|
|
||||||
// // (take suitable action for keyword "allow")
|
|
||||||
// case "deny":
|
|
||||||
// // (take suitable action for keyword "deny")
|
|
||||||
// default:
|
|
||||||
// diags = append(diags, &hcl.Diagnostic{
|
|
||||||
// // ... "invalid keyword" diagnostic message ...
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// The above approach will generate the same message for both the use of an
|
|
||||||
// unrecognized keyword and for not using a keyword at all, which is usually
|
|
||||||
// reasonable if the message specifies that the given value must be a keyword
|
|
||||||
// from that fixed list.
|
|
||||||
//
|
|
||||||
// Note that in the native syntax the keywords "true", "false", and "null" are
|
|
||||||
// recognized as literal values during parsing and so these reserved words
|
|
||||||
// cannot not be accepted as keywords by this function.
|
|
||||||
//
|
|
||||||
// Since interpreting an expression as a keyword bypasses usual expression
|
|
||||||
// evaluation, it should be used sparingly for situations where e.g. one of
|
|
||||||
// a fixed set of keywords is used in a structural way in a special attribute
|
|
||||||
// to affect the further processing of a block.
|
|
||||||
func ExprAsKeyword(expr Expression) string {
|
|
||||||
type asTraversal interface {
|
|
||||||
AsTraversal() Traversal
|
|
||||||
}
|
|
||||||
|
|
||||||
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
|
|
||||||
_, supported := expr.(asTraversal)
|
|
||||||
return supported
|
|
||||||
})
|
|
||||||
|
|
||||||
if asT, supported := physExpr.(asTraversal); supported {
|
|
||||||
if traversal := asT.AsTraversal(); len(traversal) == 1 {
|
|
||||||
return traversal.RootName()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
package hclparse
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
||||||
"github.com/hashicorp/hcl2/hcl/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NOTE: This is the public interface for parsing. The actual parsers are
|
|
||||||
// in other packages alongside this one, with this package just wrapping them
|
|
||||||
// to provide a unified interface for the caller across all supported formats.
|
|
||||||
|
|
||||||
// Parser is the main interface for parsing configuration files. As well as
|
|
||||||
// parsing files, a parser also retains a registry of all of the files it
|
|
||||||
// has parsed so that multiple attempts to parse the same file will return
|
|
||||||
// the same object and so the collected files can be used when printing
|
|
||||||
// diagnostics.
|
|
||||||
//
|
|
||||||
// Any diagnostics for parsing a file are only returned once on the first
|
|
||||||
// call to parse that file. Callers are expected to collect up diagnostics
|
|
||||||
// and present them together, so returning diagnostics for the same file
|
|
||||||
// multiple times would create a confusing result.
|
|
||||||
type Parser struct {
|
|
||||||
files map[string]*hcl.File
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewParser creates a new parser, ready to parse configuration files.
|
|
||||||
func NewParser() *Parser {
|
|
||||||
return &Parser{
|
|
||||||
files: map[string]*hcl.File{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseHCL parses the given buffer (which is assumed to have been loaded from
|
|
||||||
// the given filename) as a native-syntax configuration file and returns the
|
|
||||||
// hcl.File object representing it.
|
|
||||||
func (p *Parser) ParseHCL(src []byte, filename string) (*hcl.File, hcl.Diagnostics) {
|
|
||||||
if existing := p.files[filename]; existing != nil {
|
|
||||||
return existing, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
file, diags := hclsyntax.ParseConfig(src, filename, hcl.Pos{Byte: 0, Line: 1, Column: 1})
|
|
||||||
p.files[filename] = file
|
|
||||||
return file, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseHCLFile reads the given filename and parses it as a native-syntax HCL
|
|
||||||
// configuration file. An error diagnostic is returned if the given file
|
|
||||||
// cannot be read.
|
|
||||||
func (p *Parser) ParseHCLFile(filename string) (*hcl.File, hcl.Diagnostics) {
|
|
||||||
if existing := p.files[filename]; existing != nil {
|
|
||||||
return existing, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
src, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, hcl.Diagnostics{
|
|
||||||
{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Failed to read file",
|
|
||||||
Detail: fmt.Sprintf("The configuration file %q could not be read.", filename),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.ParseHCL(src, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseJSON parses the given JSON buffer (which is assumed to have been loaded
|
|
||||||
// from the given filename) and returns the hcl.File object representing it.
|
|
||||||
func (p *Parser) ParseJSON(src []byte, filename string) (*hcl.File, hcl.Diagnostics) {
|
|
||||||
if existing := p.files[filename]; existing != nil {
|
|
||||||
return existing, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
file, diags := json.Parse(src, filename)
|
|
||||||
p.files[filename] = file
|
|
||||||
return file, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseJSONFile reads the given filename and parses it as JSON, similarly to
|
|
||||||
// ParseJSON. An error diagnostic is returned if the given file cannot be read.
|
|
||||||
func (p *Parser) ParseJSONFile(filename string) (*hcl.File, hcl.Diagnostics) {
|
|
||||||
if existing := p.files[filename]; existing != nil {
|
|
||||||
return existing, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
file, diags := json.ParseFile(filename)
|
|
||||||
p.files[filename] = file
|
|
||||||
return file, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddFile allows a caller to record in a parser a file that was parsed some
|
|
||||||
// other way, thus allowing it to be included in the registry of sources.
|
|
||||||
func (p *Parser) AddFile(filename string, file *hcl.File) {
|
|
||||||
p.files[filename] = file
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sources returns a map from filenames to the raw source code that was
|
|
||||||
// read from them. This is intended to be used, for example, to print
|
|
||||||
// diagnostics with contextual information.
|
|
||||||
//
|
|
||||||
// The arrays underlying the returned slices should not be modified.
|
|
||||||
func (p *Parser) Sources() map[string][]byte {
|
|
||||||
ret := make(map[string][]byte)
|
|
||||||
for fn, f := range p.files {
|
|
||||||
ret[fn] = f.Bytes
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// Files returns a map from filenames to the File objects produced from them.
|
|
||||||
// This is intended to be used, for example, to print diagnostics with
|
|
||||||
// contextual information.
|
|
||||||
//
|
|
||||||
// The returned map and all of the objects it refers to directly or indirectly
|
|
||||||
// must not be modified.
|
|
||||||
func (p *Parser) Files() map[string]*hcl.File {
|
|
||||||
return p.files
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
package hclwrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
type File struct {
|
|
||||||
inTree
|
|
||||||
|
|
||||||
srcBytes []byte
|
|
||||||
body *node
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEmptyFile constructs a new file with no content, ready to be mutated
|
|
||||||
// by other calls that append to its body.
|
|
||||||
func NewEmptyFile() *File {
|
|
||||||
f := &File{
|
|
||||||
inTree: newInTree(),
|
|
||||||
}
|
|
||||||
body := newBody()
|
|
||||||
f.body = f.children.Append(body)
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
// Body returns the root body of the file, which contains the top-level
|
|
||||||
// attributes and blocks.
|
|
||||||
func (f *File) Body() *Body {
|
|
||||||
return f.body.content.(*Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTo writes the tokens underlying the receiving file to the given writer.
|
|
||||||
//
|
|
||||||
// The tokens first have a simple formatting pass applied that adjusts only
|
|
||||||
// the spaces between them.
|
|
||||||
func (f *File) WriteTo(wr io.Writer) (int64, error) {
|
|
||||||
tokens := f.inTree.children.BuildTokens(nil)
|
|
||||||
format(tokens)
|
|
||||||
return tokens.WriteTo(wr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns a buffer containing the source code resulting from the
|
|
||||||
// tokens underlying the receiving file. If any updates have been made via
|
|
||||||
// the AST API, these will be reflected in the result.
|
|
||||||
func (f *File) Bytes() []byte {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
f.WriteTo(buf)
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
type comments struct {
|
|
||||||
leafNode
|
|
||||||
|
|
||||||
parent *node
|
|
||||||
tokens Tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
func newComments(tokens Tokens) *comments {
|
|
||||||
return &comments{
|
|
||||||
tokens: tokens,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *comments) BuildTokens(to Tokens) Tokens {
|
|
||||||
return c.tokens.BuildTokens(to)
|
|
||||||
}
|
|
||||||
|
|
||||||
type identifier struct {
|
|
||||||
leafNode
|
|
||||||
|
|
||||||
parent *node
|
|
||||||
token *Token
|
|
||||||
}
|
|
||||||
|
|
||||||
func newIdentifier(token *Token) *identifier {
|
|
||||||
return &identifier{
|
|
||||||
token: token,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *identifier) BuildTokens(to Tokens) Tokens {
|
|
||||||
return append(to, i.token)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *identifier) hasName(name string) bool {
|
|
||||||
return name == string(i.token.Bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
type number struct {
|
|
||||||
leafNode
|
|
||||||
|
|
||||||
parent *node
|
|
||||||
token *Token
|
|
||||||
}
|
|
||||||
|
|
||||||
func newNumber(token *Token) *number {
|
|
||||||
return &number{
|
|
||||||
token: token,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *number) BuildTokens(to Tokens) Tokens {
|
|
||||||
return append(to, n.token)
|
|
||||||
}
|
|
||||||
|
|
||||||
type quoted struct {
|
|
||||||
leafNode
|
|
||||||
|
|
||||||
parent *node
|
|
||||||
tokens Tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
func newQuoted(tokens Tokens) *quoted {
|
|
||||||
return "ed{
|
|
||||||
tokens: tokens,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *quoted) BuildTokens(to Tokens) Tokens {
|
|
||||||
return q.tokens.BuildTokens(to)
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
package hclwrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Attribute struct {
|
|
||||||
inTree
|
|
||||||
|
|
||||||
leadComments *node
|
|
||||||
name *node
|
|
||||||
expr *node
|
|
||||||
lineComments *node
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAttribute() *Attribute {
|
|
||||||
return &Attribute{
|
|
||||||
inTree: newInTree(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Attribute) init(name string, expr *Expression) {
|
|
||||||
expr.assertUnattached()
|
|
||||||
|
|
||||||
nameTok := newIdentToken(name)
|
|
||||||
nameObj := newIdentifier(nameTok)
|
|
||||||
a.leadComments = a.children.Append(newComments(nil))
|
|
||||||
a.name = a.children.Append(nameObj)
|
|
||||||
a.children.AppendUnstructuredTokens(Tokens{
|
|
||||||
{
|
|
||||||
Type: hclsyntax.TokenEqual,
|
|
||||||
Bytes: []byte{'='},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
a.expr = a.children.Append(expr)
|
|
||||||
a.expr.list = a.children
|
|
||||||
a.lineComments = a.children.Append(newComments(nil))
|
|
||||||
a.children.AppendUnstructuredTokens(Tokens{
|
|
||||||
{
|
|
||||||
Type: hclsyntax.TokenNewline,
|
|
||||||
Bytes: []byte{'\n'},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Attribute) Expr() *Expression {
|
|
||||||
return a.expr.content.(*Expression)
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
package hclwrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Block struct {
|
|
||||||
inTree
|
|
||||||
|
|
||||||
leadComments *node
|
|
||||||
typeName *node
|
|
||||||
labels nodeSet
|
|
||||||
open *node
|
|
||||||
body *node
|
|
||||||
close *node
|
|
||||||
}
|
|
||||||
|
|
||||||
func newBlock() *Block {
|
|
||||||
return &Block{
|
|
||||||
inTree: newInTree(),
|
|
||||||
labels: newNodeSet(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBlock constructs a new, empty block with the given type name and labels.
|
|
||||||
func NewBlock(typeName string, labels []string) *Block {
|
|
||||||
block := newBlock()
|
|
||||||
block.init(typeName, labels)
|
|
||||||
return block
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Block) init(typeName string, labels []string) {
|
|
||||||
nameTok := newIdentToken(typeName)
|
|
||||||
nameObj := newIdentifier(nameTok)
|
|
||||||
b.leadComments = b.children.Append(newComments(nil))
|
|
||||||
b.typeName = b.children.Append(nameObj)
|
|
||||||
for _, label := range labels {
|
|
||||||
labelToks := TokensForValue(cty.StringVal(label))
|
|
||||||
labelObj := newQuoted(labelToks)
|
|
||||||
labelNode := b.children.Append(labelObj)
|
|
||||||
b.labels.Add(labelNode)
|
|
||||||
}
|
|
||||||
b.open = b.children.AppendUnstructuredTokens(Tokens{
|
|
||||||
{
|
|
||||||
Type: hclsyntax.TokenOBrace,
|
|
||||||
Bytes: []byte{'{'},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: hclsyntax.TokenNewline,
|
|
||||||
Bytes: []byte{'\n'},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
body := newBody() // initially totally empty; caller can append to it subsequently
|
|
||||||
b.body = b.children.Append(body)
|
|
||||||
b.close = b.children.AppendUnstructuredTokens(Tokens{
|
|
||||||
{
|
|
||||||
Type: hclsyntax.TokenCBrace,
|
|
||||||
Bytes: []byte{'}'},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: hclsyntax.TokenNewline,
|
|
||||||
Bytes: []byte{'\n'},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Body returns the body that represents the content of the receiving block.
|
|
||||||
//
|
|
||||||
// Appending to or otherwise modifying this body will make changes to the
|
|
||||||
// tokens that are generated between the blocks open and close braces.
|
|
||||||
func (b *Block) Body() *Body {
|
|
||||||
return b.body.content.(*Body)
|
|
||||||
}
|
|
|
@ -1,153 +0,0 @@
|
||||||
package hclwrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Body struct {
|
|
||||||
inTree
|
|
||||||
|
|
||||||
items nodeSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func newBody() *Body {
|
|
||||||
return &Body{
|
|
||||||
inTree: newInTree(),
|
|
||||||
items: newNodeSet(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Body) appendItem(c nodeContent) *node {
|
|
||||||
nn := b.children.Append(c)
|
|
||||||
b.items.Add(nn)
|
|
||||||
return nn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Body) appendItemNode(nn *node) *node {
|
|
||||||
nn.assertUnattached()
|
|
||||||
b.children.AppendNode(nn)
|
|
||||||
b.items.Add(nn)
|
|
||||||
return nn
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear removes all of the items from the body, making it empty.
|
|
||||||
func (b *Body) Clear() {
|
|
||||||
b.children.Clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Body) AppendUnstructuredTokens(ts Tokens) {
|
|
||||||
b.inTree.children.Append(ts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attributes returns a new map of all of the attributes in the body, with
|
|
||||||
// the attribute names as the keys.
|
|
||||||
func (b *Body) Attributes() map[string]*Attribute {
|
|
||||||
ret := make(map[string]*Attribute)
|
|
||||||
for n := range b.items {
|
|
||||||
if attr, isAttr := n.content.(*Attribute); isAttr {
|
|
||||||
nameObj := attr.name.content.(*identifier)
|
|
||||||
name := string(nameObj.token.Bytes)
|
|
||||||
ret[name] = attr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blocks returns a new slice of all the blocks in the body.
|
|
||||||
func (b *Body) Blocks() []*Block {
|
|
||||||
ret := make([]*Block, 0, len(b.items))
|
|
||||||
for n := range b.items {
|
|
||||||
if block, isBlock := n.content.(*Block); isBlock {
|
|
||||||
ret = append(ret, block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAttribute returns the attribute from the body that has the given name,
|
|
||||||
// or returns nil if there is currently no matching attribute.
|
|
||||||
func (b *Body) GetAttribute(name string) *Attribute {
|
|
||||||
for n := range b.items {
|
|
||||||
if attr, isAttr := n.content.(*Attribute); isAttr {
|
|
||||||
nameObj := attr.name.content.(*identifier)
|
|
||||||
if nameObj.hasName(name) {
|
|
||||||
// We've found it!
|
|
||||||
return attr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAttributeValue either replaces the expression of an existing attribute
|
|
||||||
// of the given name or adds a new attribute definition to the end of the block.
|
|
||||||
//
|
|
||||||
// The value is given as a cty.Value, and must therefore be a literal. To set
|
|
||||||
// a variable reference or other traversal, use SetAttributeTraversal.
|
|
||||||
//
|
|
||||||
// The return value is the attribute that was either modified in-place or
|
|
||||||
// created.
|
|
||||||
func (b *Body) SetAttributeValue(name string, val cty.Value) *Attribute {
|
|
||||||
attr := b.GetAttribute(name)
|
|
||||||
expr := NewExpressionLiteral(val)
|
|
||||||
if attr != nil {
|
|
||||||
attr.expr = attr.expr.ReplaceWith(expr)
|
|
||||||
} else {
|
|
||||||
attr := newAttribute()
|
|
||||||
attr.init(name, expr)
|
|
||||||
b.appendItem(attr)
|
|
||||||
}
|
|
||||||
return attr
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAttributeTraversal either replaces the expression of an existing attribute
|
|
||||||
// of the given name or adds a new attribute definition to the end of the body.
|
|
||||||
//
|
|
||||||
// The new expression is given as a hcl.Traversal, which must be an absolute
|
|
||||||
// traversal. To set a literal value, use SetAttributeValue.
|
|
||||||
//
|
|
||||||
// The return value is the attribute that was either modified in-place or
|
|
||||||
// created.
|
|
||||||
func (b *Body) SetAttributeTraversal(name string, traversal hcl.Traversal) *Attribute {
|
|
||||||
attr := b.GetAttribute(name)
|
|
||||||
expr := NewExpressionAbsTraversal(traversal)
|
|
||||||
if attr != nil {
|
|
||||||
attr.expr = attr.expr.ReplaceWith(expr)
|
|
||||||
} else {
|
|
||||||
attr := newAttribute()
|
|
||||||
attr.init(name, expr)
|
|
||||||
b.appendItem(attr)
|
|
||||||
}
|
|
||||||
return attr
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendBlock appends an existing block (which must not be already attached
|
|
||||||
// to a body) to the end of the receiving body.
|
|
||||||
func (b *Body) AppendBlock(block *Block) *Block {
|
|
||||||
b.appendItem(block)
|
|
||||||
return block
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendNewBlock appends a new nested block to the end of the receiving body
|
|
||||||
// with the given type name and labels.
|
|
||||||
func (b *Body) AppendNewBlock(typeName string, labels []string) *Block {
|
|
||||||
block := newBlock()
|
|
||||||
block.init(typeName, labels)
|
|
||||||
b.appendItem(block)
|
|
||||||
return block
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendNewline appends a newline token to th end of the receiving body,
|
|
||||||
// which generally serves as a separator between different sets of body
|
|
||||||
// contents.
|
|
||||||
func (b *Body) AppendNewline() {
|
|
||||||
b.AppendUnstructuredTokens(Tokens{
|
|
||||||
{
|
|
||||||
Type: hclsyntax.TokenNewline,
|
|
||||||
Bytes: []byte{'\n'},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,201 +0,0 @@
|
||||||
package hclwrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Expression struct {
|
|
||||||
inTree
|
|
||||||
|
|
||||||
absTraversals nodeSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func newExpression() *Expression {
|
|
||||||
return &Expression{
|
|
||||||
inTree: newInTree(),
|
|
||||||
absTraversals: newNodeSet(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExpressionLiteral constructs an an expression that represents the given
|
|
||||||
// literal value.
|
|
||||||
//
|
|
||||||
// Since an unknown value cannot be represented in source code, this function
|
|
||||||
// will panic if the given value is unknown or contains a nested unknown value.
|
|
||||||
// Use val.IsWhollyKnown before calling to be sure.
|
|
||||||
//
|
|
||||||
// HCL native syntax does not directly represent lists, maps, and sets, and
|
|
||||||
// instead relies on the automatic conversions to those collection types from
|
|
||||||
// either list or tuple constructor syntax. Therefore converting collection
|
|
||||||
// values to source code and re-reading them will lose type information, and
|
|
||||||
// the reader must provide a suitable type at decode time to recover the
|
|
||||||
// original value.
|
|
||||||
func NewExpressionLiteral(val cty.Value) *Expression {
|
|
||||||
toks := TokensForValue(val)
|
|
||||||
expr := newExpression()
|
|
||||||
expr.children.AppendUnstructuredTokens(toks)
|
|
||||||
return expr
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExpressionAbsTraversal constructs an expression that represents the
|
|
||||||
// given traversal, which must be absolute or this function will panic.
|
|
||||||
func NewExpressionAbsTraversal(traversal hcl.Traversal) *Expression {
|
|
||||||
if traversal.IsRelative() {
|
|
||||||
panic("can't construct expression from relative traversal")
|
|
||||||
}
|
|
||||||
|
|
||||||
physT := newTraversal()
|
|
||||||
rootName := traversal.RootName()
|
|
||||||
steps := traversal[1:]
|
|
||||||
|
|
||||||
{
|
|
||||||
tn := newTraverseName()
|
|
||||||
tn.name = tn.children.Append(newIdentifier(&Token{
|
|
||||||
Type: hclsyntax.TokenIdent,
|
|
||||||
Bytes: []byte(rootName),
|
|
||||||
}))
|
|
||||||
physT.steps.Add(physT.children.Append(tn))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, step := range steps {
|
|
||||||
switch ts := step.(type) {
|
|
||||||
case hcl.TraverseAttr:
|
|
||||||
tn := newTraverseName()
|
|
||||||
tn.children.AppendUnstructuredTokens(Tokens{
|
|
||||||
{
|
|
||||||
Type: hclsyntax.TokenDot,
|
|
||||||
Bytes: []byte{'.'},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
tn.name = tn.children.Append(newIdentifier(&Token{
|
|
||||||
Type: hclsyntax.TokenIdent,
|
|
||||||
Bytes: []byte(ts.Name),
|
|
||||||
}))
|
|
||||||
physT.steps.Add(physT.children.Append(tn))
|
|
||||||
case hcl.TraverseIndex:
|
|
||||||
ti := newTraverseIndex()
|
|
||||||
ti.children.AppendUnstructuredTokens(Tokens{
|
|
||||||
{
|
|
||||||
Type: hclsyntax.TokenOBrack,
|
|
||||||
Bytes: []byte{'['},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
indexExpr := NewExpressionLiteral(ts.Key)
|
|
||||||
ti.key = ti.children.Append(indexExpr)
|
|
||||||
ti.children.AppendUnstructuredTokens(Tokens{
|
|
||||||
{
|
|
||||||
Type: hclsyntax.TokenCBrack,
|
|
||||||
Bytes: []byte{']'},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
physT.steps.Add(physT.children.Append(ti))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expr := newExpression()
|
|
||||||
expr.absTraversals.Add(expr.children.Append(physT))
|
|
||||||
return expr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Variables returns the absolute traversals that exist within the receiving
|
|
||||||
// expression.
|
|
||||||
func (e *Expression) Variables() []*Traversal {
|
|
||||||
nodes := e.absTraversals.List()
|
|
||||||
ret := make([]*Traversal, len(nodes))
|
|
||||||
for i, node := range nodes {
|
|
||||||
ret[i] = node.content.(*Traversal)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenameVariablePrefix examines each of the absolute traversals in the
|
|
||||||
// receiving expression to see if they have the given sequence of names as
|
|
||||||
// a prefix prefix. If so, they are updated in place to have the given
|
|
||||||
// replacement names instead of that prefix.
|
|
||||||
//
|
|
||||||
// This can be used to implement symbol renaming. The calling application can
|
|
||||||
// visit all relevant expressions in its input and apply the same renaming
|
|
||||||
// to implement a global symbol rename.
|
|
||||||
//
|
|
||||||
// The search and replacement traversals must be the same length, or this
|
|
||||||
// method will panic. Only attribute access operations can be matched and
|
|
||||||
// replaced. Index steps never match the prefix.
|
|
||||||
func (e *Expression) RenameVariablePrefix(search, replacement []string) {
|
|
||||||
if len(search) != len(replacement) {
|
|
||||||
panic(fmt.Sprintf("search and replacement length mismatch (%d and %d)", len(search), len(replacement)))
|
|
||||||
}
|
|
||||||
Traversals:
|
|
||||||
for node := range e.absTraversals {
|
|
||||||
traversal := node.content.(*Traversal)
|
|
||||||
if len(traversal.steps) < len(search) {
|
|
||||||
// If it's shorter then it can't have our prefix
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
stepNodes := traversal.steps.List()
|
|
||||||
for i, name := range search {
|
|
||||||
step, isName := stepNodes[i].content.(*TraverseName)
|
|
||||||
if !isName {
|
|
||||||
continue Traversals // only name nodes can match
|
|
||||||
}
|
|
||||||
foundNameBytes := step.name.content.(*identifier).token.Bytes
|
|
||||||
if len(foundNameBytes) != len(name) {
|
|
||||||
continue Traversals
|
|
||||||
}
|
|
||||||
if string(foundNameBytes) != name {
|
|
||||||
continue Traversals
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get here then the prefix matched, so now we'll swap in
|
|
||||||
// the replacement strings.
|
|
||||||
for i, name := range replacement {
|
|
||||||
step := stepNodes[i].content.(*TraverseName)
|
|
||||||
token := step.name.content.(*identifier).token
|
|
||||||
token.Bytes = []byte(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traversal represents a sequence of variable, attribute, and/or index
|
|
||||||
// operations.
|
|
||||||
type Traversal struct {
|
|
||||||
inTree
|
|
||||||
|
|
||||||
steps nodeSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTraversal() *Traversal {
|
|
||||||
return &Traversal{
|
|
||||||
inTree: newInTree(),
|
|
||||||
steps: newNodeSet(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TraverseName struct {
|
|
||||||
inTree
|
|
||||||
|
|
||||||
name *node
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTraverseName() *TraverseName {
|
|
||||||
return &TraverseName{
|
|
||||||
inTree: newInTree(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TraverseIndex struct {
|
|
||||||
inTree
|
|
||||||
|
|
||||||
key *node
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTraverseIndex() *TraverseIndex {
|
|
||||||
return &TraverseIndex{
|
|
||||||
inTree: newInTree(),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
// Package hclwrite deals with the problem of generating HCL configuration
|
|
||||||
// and of making specific surgical changes to existing HCL configurations.
|
|
||||||
//
|
|
||||||
// It operates at a different level of abstraction than the main HCL parser
|
|
||||||
// and AST, since details such as the placement of comments and newlines
|
|
||||||
// are preserved when unchanged.
|
|
||||||
//
|
|
||||||
// The hclwrite API follows a similar principle to XML/HTML DOM, allowing nodes
|
|
||||||
// to be read out, created and inserted, etc. Nodes represent syntax constructs
|
|
||||||
// rather than semantic concepts.
|
|
||||||
package hclwrite
|
|
|
@ -1,463 +0,0 @@
|
||||||
package hclwrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
||||||
)
|
|
||||||
|
|
||||||
var inKeyword = hclsyntax.Keyword([]byte{'i', 'n'})
|
|
||||||
|
|
||||||
// placeholder token used when we don't have a token but we don't want
|
|
||||||
// to pass a real "nil" and complicate things with nil pointer checks
|
|
||||||
var nilToken = &Token{
|
|
||||||
Type: hclsyntax.TokenNil,
|
|
||||||
Bytes: []byte{},
|
|
||||||
SpacesBefore: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
// format rewrites tokens within the given sequence, in-place, to adjust the
|
|
||||||
// whitespace around their content to achieve canonical formatting.
|
|
||||||
func format(tokens Tokens) {
|
|
||||||
// Formatting is a multi-pass process. More details on the passes below,
|
|
||||||
// but this is the overview:
|
|
||||||
// - adjust the leading space on each line to create appropriate
|
|
||||||
// indentation
|
|
||||||
// - adjust spaces between tokens in a single cell using a set of rules
|
|
||||||
// - adjust the leading space in the "assign" and "comment" cells on each
|
|
||||||
// line to vertically align with neighboring lines.
|
|
||||||
// All of these steps operate in-place on the given tokens, so a caller
|
|
||||||
// may collect a flat sequence of all of the tokens underlying an AST
|
|
||||||
// and pass it here and we will then indirectly modify the AST itself.
|
|
||||||
// Formatting must change only whitespace. Specifically, that means
|
|
||||||
// changing the SpacesBefore attribute on a token while leaving the
|
|
||||||
// other token attributes unchanged.
|
|
||||||
|
|
||||||
lines := linesForFormat(tokens)
|
|
||||||
formatIndent(lines)
|
|
||||||
formatSpaces(lines)
|
|
||||||
formatCells(lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatIndent(lines []formatLine) {
|
|
||||||
// Our methodology for indents is to take the input one line at a time
|
|
||||||
// and count the bracketing delimiters on each line. If a line has a net
|
|
||||||
// increase in open brackets, we increase the indent level by one and
|
|
||||||
// remember how many new openers we had. If the line has a net _decrease_,
|
|
||||||
// we'll compare it to the most recent number of openers and decrease the
|
|
||||||
// dedent level by one each time we pass an indent level remembered
|
|
||||||
// earlier.
|
|
||||||
// The "indent stack" used here allows for us to recognize degenerate
|
|
||||||
// input where brackets are not symmetrical within lines and avoid
|
|
||||||
// pushing things too far left or right, creating confusion.
|
|
||||||
|
|
||||||
// We'll start our indent stack at a reasonable capacity to minimize the
|
|
||||||
// chance of us needing to grow it; 10 here means 10 levels of indent,
|
|
||||||
// which should be more than enough for reasonable HCL uses.
|
|
||||||
indents := make([]int, 0, 10)
|
|
||||||
|
|
||||||
for i := range lines {
|
|
||||||
line := &lines[i]
|
|
||||||
if len(line.lead) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if line.lead[0].Type == hclsyntax.TokenNewline {
|
|
||||||
// Never place spaces before a newline
|
|
||||||
line.lead[0].SpacesBefore = 0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
netBrackets := 0
|
|
||||||
for _, token := range line.lead {
|
|
||||||
netBrackets += tokenBracketChange(token)
|
|
||||||
if token.Type == hclsyntax.TokenOHeredoc {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, token := range line.assign {
|
|
||||||
netBrackets += tokenBracketChange(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case netBrackets > 0:
|
|
||||||
line.lead[0].SpacesBefore = 2 * len(indents)
|
|
||||||
indents = append(indents, netBrackets)
|
|
||||||
case netBrackets < 0:
|
|
||||||
closed := -netBrackets
|
|
||||||
for closed > 0 && len(indents) > 0 {
|
|
||||||
switch {
|
|
||||||
|
|
||||||
case closed > indents[len(indents)-1]:
|
|
||||||
closed -= indents[len(indents)-1]
|
|
||||||
indents = indents[:len(indents)-1]
|
|
||||||
|
|
||||||
case closed < indents[len(indents)-1]:
|
|
||||||
indents[len(indents)-1] -= closed
|
|
||||||
closed = 0
|
|
||||||
|
|
||||||
default:
|
|
||||||
indents = indents[:len(indents)-1]
|
|
||||||
closed = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
line.lead[0].SpacesBefore = 2 * len(indents)
|
|
||||||
default:
|
|
||||||
line.lead[0].SpacesBefore = 2 * len(indents)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatSpaces(lines []formatLine) {
|
|
||||||
for _, line := range lines {
|
|
||||||
for i, token := range line.lead {
|
|
||||||
var before, after *Token
|
|
||||||
if i > 0 {
|
|
||||||
before = line.lead[i-1]
|
|
||||||
} else {
|
|
||||||
before = nilToken
|
|
||||||
}
|
|
||||||
if i < (len(line.lead) - 1) {
|
|
||||||
after = line.lead[i+1]
|
|
||||||
} else {
|
|
||||||
after = nilToken
|
|
||||||
}
|
|
||||||
if spaceAfterToken(token, before, after) {
|
|
||||||
after.SpacesBefore = 1
|
|
||||||
} else {
|
|
||||||
after.SpacesBefore = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, token := range line.assign {
|
|
||||||
if i == 0 {
|
|
||||||
// first token in "assign" always has one space before to
|
|
||||||
// separate the equals sign from what it's assigning.
|
|
||||||
token.SpacesBefore = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var before, after *Token
|
|
||||||
if i > 0 {
|
|
||||||
before = line.assign[i-1]
|
|
||||||
} else {
|
|
||||||
before = nilToken
|
|
||||||
}
|
|
||||||
if i < (len(line.assign) - 1) {
|
|
||||||
after = line.assign[i+1]
|
|
||||||
} else {
|
|
||||||
after = nilToken
|
|
||||||
}
|
|
||||||
if spaceAfterToken(token, before, after) {
|
|
||||||
after.SpacesBefore = 1
|
|
||||||
} else {
|
|
||||||
after.SpacesBefore = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatCells(lines []formatLine) {
|
|
||||||
|
|
||||||
chainStart := -1
|
|
||||||
maxColumns := 0
|
|
||||||
|
|
||||||
// We'll deal with the "assign" cell first, since moving that will
|
|
||||||
// also impact the "comment" cell.
|
|
||||||
closeAssignChain := func(i int) {
|
|
||||||
for _, chainLine := range lines[chainStart:i] {
|
|
||||||
columns := chainLine.lead.Columns()
|
|
||||||
spaces := (maxColumns - columns) + 1
|
|
||||||
chainLine.assign[0].SpacesBefore = spaces
|
|
||||||
}
|
|
||||||
chainStart = -1
|
|
||||||
maxColumns = 0
|
|
||||||
}
|
|
||||||
for i, line := range lines {
|
|
||||||
if line.assign == nil {
|
|
||||||
if chainStart != -1 {
|
|
||||||
closeAssignChain(i)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if chainStart == -1 {
|
|
||||||
chainStart = i
|
|
||||||
}
|
|
||||||
columns := line.lead.Columns()
|
|
||||||
if columns > maxColumns {
|
|
||||||
maxColumns = columns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if chainStart != -1 {
|
|
||||||
closeAssignChain(len(lines))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we'll deal with the comments
|
|
||||||
closeCommentChain := func(i int) {
|
|
||||||
for _, chainLine := range lines[chainStart:i] {
|
|
||||||
columns := chainLine.lead.Columns() + chainLine.assign.Columns()
|
|
||||||
spaces := (maxColumns - columns) + 1
|
|
||||||
chainLine.comment[0].SpacesBefore = spaces
|
|
||||||
}
|
|
||||||
chainStart = -1
|
|
||||||
maxColumns = 0
|
|
||||||
}
|
|
||||||
for i, line := range lines {
|
|
||||||
if line.comment == nil {
|
|
||||||
if chainStart != -1 {
|
|
||||||
closeCommentChain(i)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if chainStart == -1 {
|
|
||||||
chainStart = i
|
|
||||||
}
|
|
||||||
columns := line.lead.Columns() + line.assign.Columns()
|
|
||||||
if columns > maxColumns {
|
|
||||||
maxColumns = columns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if chainStart != -1 {
|
|
||||||
closeCommentChain(len(lines))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// spaceAfterToken decides whether a particular subject token should have a
|
|
||||||
// space after it when surrounded by the given before and after tokens.
|
|
||||||
// "before" can be TokenNil, if the subject token is at the start of a sequence.
|
|
||||||
func spaceAfterToken(subject, before, after *Token) bool {
|
|
||||||
switch {
|
|
||||||
|
|
||||||
case after.Type == hclsyntax.TokenNewline || after.Type == hclsyntax.TokenNil:
|
|
||||||
// Never add spaces before a newline
|
|
||||||
return false
|
|
||||||
|
|
||||||
case subject.Type == hclsyntax.TokenIdent && after.Type == hclsyntax.TokenOParen:
|
|
||||||
// Don't split a function name from open paren in a call
|
|
||||||
return false
|
|
||||||
|
|
||||||
case subject.Type == hclsyntax.TokenDot || after.Type == hclsyntax.TokenDot:
|
|
||||||
// Don't use spaces around attribute access dots
|
|
||||||
return false
|
|
||||||
|
|
||||||
case after.Type == hclsyntax.TokenComma || after.Type == hclsyntax.TokenEllipsis:
|
|
||||||
// No space right before a comma or ... in an argument list
|
|
||||||
return false
|
|
||||||
|
|
||||||
case subject.Type == hclsyntax.TokenComma:
|
|
||||||
// Always a space after a comma
|
|
||||||
return true
|
|
||||||
|
|
||||||
case subject.Type == hclsyntax.TokenQuotedLit || subject.Type == hclsyntax.TokenStringLit || subject.Type == hclsyntax.TokenOQuote || subject.Type == hclsyntax.TokenOHeredoc || after.Type == hclsyntax.TokenQuotedLit || after.Type == hclsyntax.TokenStringLit || after.Type == hclsyntax.TokenCQuote || after.Type == hclsyntax.TokenCHeredoc:
|
|
||||||
// No extra spaces within templates
|
|
||||||
return false
|
|
||||||
|
|
||||||
case inKeyword.TokenMatches(subject.asHCLSyntax()) && before.Type == hclsyntax.TokenIdent:
|
|
||||||
// This is a special case for inside for expressions where a user
|
|
||||||
// might want to use a literal tuple constructor:
|
|
||||||
// [for x in [foo]: x]
|
|
||||||
// ... in that case, we would normally produce in[foo] thinking that
|
|
||||||
// in is a reference, but we'll recognize it as a keyword here instead
|
|
||||||
// to make the result less confusing.
|
|
||||||
return true
|
|
||||||
|
|
||||||
case after.Type == hclsyntax.TokenOBrack && (subject.Type == hclsyntax.TokenIdent || subject.Type == hclsyntax.TokenNumberLit || tokenBracketChange(subject) < 0):
|
|
||||||
return false
|
|
||||||
|
|
||||||
case subject.Type == hclsyntax.TokenMinus:
|
|
||||||
// Since a minus can either be subtraction or negation, and the latter
|
|
||||||
// should _not_ have a space after it, we need to use some heuristics
|
|
||||||
// to decide which case this is.
|
|
||||||
// We guess that we have a negation if the token before doesn't look
|
|
||||||
// like it could be the end of an expression.
|
|
||||||
|
|
||||||
switch before.Type {
|
|
||||||
|
|
||||||
case hclsyntax.TokenNil:
|
|
||||||
// Minus at the start of input must be a negation
|
|
||||||
return false
|
|
||||||
|
|
||||||
case hclsyntax.TokenOParen, hclsyntax.TokenOBrace, hclsyntax.TokenOBrack, hclsyntax.TokenEqual, hclsyntax.TokenColon, hclsyntax.TokenComma, hclsyntax.TokenQuestion:
|
|
||||||
// Minus immediately after an opening bracket or separator must be a negation.
|
|
||||||
return false
|
|
||||||
|
|
||||||
case hclsyntax.TokenPlus, hclsyntax.TokenStar, hclsyntax.TokenSlash, hclsyntax.TokenPercent, hclsyntax.TokenMinus:
|
|
||||||
// Minus immediately after another arithmetic operator must be negation.
|
|
||||||
return false
|
|
||||||
|
|
||||||
case hclsyntax.TokenEqualOp, hclsyntax.TokenNotEqual, hclsyntax.TokenGreaterThan, hclsyntax.TokenGreaterThanEq, hclsyntax.TokenLessThan, hclsyntax.TokenLessThanEq:
|
|
||||||
// Minus immediately after another comparison operator must be negation.
|
|
||||||
return false
|
|
||||||
|
|
||||||
case hclsyntax.TokenAnd, hclsyntax.TokenOr, hclsyntax.TokenBang:
|
|
||||||
// Minus immediately after logical operator doesn't make sense but probably intended as negation.
|
|
||||||
return false
|
|
||||||
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
case subject.Type == hclsyntax.TokenOBrace || after.Type == hclsyntax.TokenCBrace:
|
|
||||||
// Unlike other bracket types, braces have spaces on both sides of them,
|
|
||||||
// both in single-line nested blocks foo { bar = baz } and in object
|
|
||||||
// constructor expressions foo = { bar = baz }.
|
|
||||||
if subject.Type == hclsyntax.TokenOBrace && after.Type == hclsyntax.TokenCBrace {
|
|
||||||
// An open brace followed by a close brace is an exception, however.
|
|
||||||
// e.g. foo {} rather than foo { }
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
|
|
||||||
// In the unlikely event that an interpolation expression is just
|
|
||||||
// a single object constructor, we'll put a space between the ${ and
|
|
||||||
// the following { to make this more obvious, and then the same
|
|
||||||
// thing for the two braces at the end.
|
|
||||||
case (subject.Type == hclsyntax.TokenTemplateInterp || subject.Type == hclsyntax.TokenTemplateControl) && after.Type == hclsyntax.TokenOBrace:
|
|
||||||
return true
|
|
||||||
case subject.Type == hclsyntax.TokenCBrace && after.Type == hclsyntax.TokenTemplateSeqEnd:
|
|
||||||
return true
|
|
||||||
|
|
||||||
// Don't add spaces between interpolated items
|
|
||||||
case subject.Type == hclsyntax.TokenTemplateSeqEnd && (after.Type == hclsyntax.TokenTemplateInterp || after.Type == hclsyntax.TokenTemplateControl):
|
|
||||||
return false
|
|
||||||
|
|
||||||
case tokenBracketChange(subject) > 0:
|
|
||||||
// No spaces after open brackets
|
|
||||||
return false
|
|
||||||
|
|
||||||
case tokenBracketChange(after) < 0:
|
|
||||||
// No spaces before close brackets
|
|
||||||
return false
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Most tokens are space-separated
|
|
||||||
return true
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func linesForFormat(tokens Tokens) []formatLine {
|
|
||||||
if len(tokens) == 0 {
|
|
||||||
return make([]formatLine, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// first we'll count our lines, so we can allocate the array for them in
|
|
||||||
// a single block. (We want to minimize memory pressure in this codepath,
|
|
||||||
// so it can be run somewhat-frequently by editor integrations.)
|
|
||||||
lineCount := 1 // if there are zero newlines then there is one line
|
|
||||||
for _, tok := range tokens {
|
|
||||||
if tokenIsNewline(tok) {
|
|
||||||
lineCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// To start, we'll just put everything in the "lead" cell on each line,
|
|
||||||
// and then do another pass over the lines afterwards to adjust.
|
|
||||||
lines := make([]formatLine, lineCount)
|
|
||||||
li := 0
|
|
||||||
lineStart := 0
|
|
||||||
for i, tok := range tokens {
|
|
||||||
if tok.Type == hclsyntax.TokenEOF {
|
|
||||||
// The EOF token doesn't belong to any line, and terminates the
|
|
||||||
// token sequence.
|
|
||||||
lines[li].lead = tokens[lineStart:i]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokenIsNewline(tok) {
|
|
||||||
lines[li].lead = tokens[lineStart : i+1]
|
|
||||||
lineStart = i + 1
|
|
||||||
li++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a set of tokens doesn't end in TokenEOF (e.g. because it's a
|
|
||||||
// fragment of tokens from the middle of a file) then we might fall
|
|
||||||
// out here with a line still pending.
|
|
||||||
if lineStart < len(tokens) {
|
|
||||||
lines[li].lead = tokens[lineStart:]
|
|
||||||
if lines[li].lead[len(lines[li].lead)-1].Type == hclsyntax.TokenEOF {
|
|
||||||
lines[li].lead = lines[li].lead[:len(lines[li].lead)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we'll pick off any trailing comments and attribute assignments
|
|
||||||
// to shuffle off into the "comment" and "assign" cells.
|
|
||||||
for i := range lines {
|
|
||||||
line := &lines[i]
|
|
||||||
|
|
||||||
if len(line.lead) == 0 {
|
|
||||||
// if the line is empty then there's nothing for us to do
|
|
||||||
// (this should happen only for the final line, because all other
|
|
||||||
// lines would have a newline token of some kind)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(line.lead) > 1 && line.lead[len(line.lead)-1].Type == hclsyntax.TokenComment {
|
|
||||||
line.comment = line.lead[len(line.lead)-1:]
|
|
||||||
line.lead = line.lead[:len(line.lead)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tok := range line.lead {
|
|
||||||
if i > 0 && tok.Type == hclsyntax.TokenEqual {
|
|
||||||
// We only move the tokens into "assign" if the RHS seems to
|
|
||||||
// be a whole expression, which we determine by counting
|
|
||||||
// brackets. If there's a net positive number of brackets
|
|
||||||
// then that suggests we're introducing a multi-line expression.
|
|
||||||
netBrackets := 0
|
|
||||||
for _, token := range line.lead[i:] {
|
|
||||||
netBrackets += tokenBracketChange(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
if netBrackets == 0 {
|
|
||||||
line.assign = line.lead[i:]
|
|
||||||
line.lead = line.lead[:i]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
|
|
||||||
func tokenIsNewline(tok *Token) bool {
|
|
||||||
if tok.Type == hclsyntax.TokenNewline {
|
|
||||||
return true
|
|
||||||
} else if tok.Type == hclsyntax.TokenComment {
|
|
||||||
// Single line tokens (# and //) consume their terminating newline,
|
|
||||||
// so we need to treat them as newline tokens as well.
|
|
||||||
if len(tok.Bytes) > 0 && tok.Bytes[len(tok.Bytes)-1] == '\n' {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func tokenBracketChange(tok *Token) int {
|
|
||||||
switch tok.Type {
|
|
||||||
case hclsyntax.TokenOBrace, hclsyntax.TokenOBrack, hclsyntax.TokenOParen, hclsyntax.TokenTemplateControl, hclsyntax.TokenTemplateInterp:
|
|
||||||
return 1
|
|
||||||
case hclsyntax.TokenCBrace, hclsyntax.TokenCBrack, hclsyntax.TokenCParen, hclsyntax.TokenTemplateSeqEnd:
|
|
||||||
return -1
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatLine represents a single line of source code for formatting purposes,
|
|
||||||
// splitting its tokens into up to three "cells":
|
|
||||||
//
|
|
||||||
// lead: always present, representing everything up to one of the others
|
|
||||||
// assign: if line contains an attribute assignment, represents the tokens
|
|
||||||
// starting at (and including) the equals symbol
|
|
||||||
// comment: if line contains any non-comment tokens and ends with a
|
|
||||||
// single-line comment token, represents the comment.
|
|
||||||
//
|
|
||||||
// When formatting, the leading spaces of the first tokens in each of these
|
|
||||||
// cells is adjusted to align vertically their occurences on consecutive
|
|
||||||
// rows.
|
|
||||||
type formatLine struct {
|
|
||||||
lead Tokens
|
|
||||||
assign Tokens
|
|
||||||
comment Tokens
|
|
||||||
}
|
|
|
@ -1,250 +0,0 @@
|
||||||
package hclwrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"unicode"
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TokensForValue returns a sequence of tokens that represents the given
|
|
||||||
// constant value.
|
|
||||||
//
|
|
||||||
// This function only supports types that are used by HCL. In particular, it
|
|
||||||
// does not support capsule types and will panic if given one.
|
|
||||||
//
|
|
||||||
// It is not possible to express an unknown value in source code, so this
|
|
||||||
// function will panic if the given value is unknown or contains any unknown
|
|
||||||
// values. A caller can call the value's IsWhollyKnown method to verify that
|
|
||||||
// no unknown values are present before calling TokensForValue.
|
|
||||||
func TokensForValue(val cty.Value) Tokens {
|
|
||||||
toks := appendTokensForValue(val, nil)
|
|
||||||
format(toks) // fiddle with the SpacesBefore field to get canonical spacing
|
|
||||||
return toks
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokensForTraversal returns a sequence of tokens that represents the given
|
|
||||||
// traversal.
|
|
||||||
//
|
|
||||||
// If the traversal is absolute then the result is a self-contained, valid
|
|
||||||
// reference expression. If the traversal is relative then the returned tokens
|
|
||||||
// could be appended to some other expression tokens to traverse into the
|
|
||||||
// represented expression.
|
|
||||||
func TokensForTraversal(traversal hcl.Traversal) Tokens {
|
|
||||||
toks := appendTokensForTraversal(traversal, nil)
|
|
||||||
format(toks) // fiddle with the SpacesBefore field to get canonical spacing
|
|
||||||
return toks
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendTokensForValue(val cty.Value, toks Tokens) Tokens {
|
|
||||||
switch {
|
|
||||||
|
|
||||||
case !val.IsKnown():
|
|
||||||
panic("cannot produce tokens for unknown value")
|
|
||||||
|
|
||||||
case val.IsNull():
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenIdent,
|
|
||||||
Bytes: []byte(`null`),
|
|
||||||
})
|
|
||||||
|
|
||||||
case val.Type() == cty.Bool:
|
|
||||||
var src []byte
|
|
||||||
if val.True() {
|
|
||||||
src = []byte(`true`)
|
|
||||||
} else {
|
|
||||||
src = []byte(`false`)
|
|
||||||
}
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenIdent,
|
|
||||||
Bytes: src,
|
|
||||||
})
|
|
||||||
|
|
||||||
case val.Type() == cty.Number:
|
|
||||||
bf := val.AsBigFloat()
|
|
||||||
srcStr := bf.Text('f', -1)
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenNumberLit,
|
|
||||||
Bytes: []byte(srcStr),
|
|
||||||
})
|
|
||||||
|
|
||||||
case val.Type() == cty.String:
|
|
||||||
// TODO: If it's a multi-line string ending in a newline, format
|
|
||||||
// it as a HEREDOC instead.
|
|
||||||
src := escapeQuotedStringLit(val.AsString())
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenOQuote,
|
|
||||||
Bytes: []byte{'"'},
|
|
||||||
})
|
|
||||||
if len(src) > 0 {
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenQuotedLit,
|
|
||||||
Bytes: src,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenCQuote,
|
|
||||||
Bytes: []byte{'"'},
|
|
||||||
})
|
|
||||||
|
|
||||||
case val.Type().IsListType() || val.Type().IsSetType() || val.Type().IsTupleType():
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenOBrack,
|
|
||||||
Bytes: []byte{'['},
|
|
||||||
})
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
for it := val.ElementIterator(); it.Next(); {
|
|
||||||
if i > 0 {
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenComma,
|
|
||||||
Bytes: []byte{','},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_, eVal := it.Element()
|
|
||||||
toks = appendTokensForValue(eVal, toks)
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenCBrack,
|
|
||||||
Bytes: []byte{']'},
|
|
||||||
})
|
|
||||||
|
|
||||||
case val.Type().IsMapType() || val.Type().IsObjectType():
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenOBrace,
|
|
||||||
Bytes: []byte{'{'},
|
|
||||||
})
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
for it := val.ElementIterator(); it.Next(); {
|
|
||||||
if i > 0 {
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenComma,
|
|
||||||
Bytes: []byte{','},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
eKey, eVal := it.Element()
|
|
||||||
if hclsyntax.ValidIdentifier(eKey.AsString()) {
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenIdent,
|
|
||||||
Bytes: []byte(eKey.AsString()),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
toks = appendTokensForValue(eKey, toks)
|
|
||||||
}
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenEqual,
|
|
||||||
Bytes: []byte{'='},
|
|
||||||
})
|
|
||||||
toks = appendTokensForValue(eVal, toks)
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenCBrace,
|
|
||||||
Bytes: []byte{'}'},
|
|
||||||
})
|
|
||||||
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("cannot produce tokens for %#v", val))
|
|
||||||
}
|
|
||||||
|
|
||||||
return toks
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendTokensForTraversal(traversal hcl.Traversal, toks Tokens) Tokens {
|
|
||||||
for _, step := range traversal {
|
|
||||||
appendTokensForTraversalStep(step, toks)
|
|
||||||
}
|
|
||||||
return toks
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendTokensForTraversalStep(step hcl.Traverser, toks Tokens) {
|
|
||||||
switch ts := step.(type) {
|
|
||||||
case hcl.TraverseRoot:
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenIdent,
|
|
||||||
Bytes: []byte(ts.Name),
|
|
||||||
})
|
|
||||||
case hcl.TraverseAttr:
|
|
||||||
toks = append(
|
|
||||||
toks,
|
|
||||||
&Token{
|
|
||||||
Type: hclsyntax.TokenDot,
|
|
||||||
Bytes: []byte{'.'},
|
|
||||||
},
|
|
||||||
&Token{
|
|
||||||
Type: hclsyntax.TokenIdent,
|
|
||||||
Bytes: []byte(ts.Name),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
case hcl.TraverseIndex:
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenOBrack,
|
|
||||||
Bytes: []byte{'['},
|
|
||||||
})
|
|
||||||
appendTokensForValue(ts.Key, toks)
|
|
||||||
toks = append(toks, &Token{
|
|
||||||
Type: hclsyntax.TokenCBrack,
|
|
||||||
Bytes: []byte{']'},
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unsupported traversal step type %T", step))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func escapeQuotedStringLit(s string) []byte {
|
|
||||||
if len(s) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
buf := make([]byte, 0, len(s))
|
|
||||||
for i, r := range s {
|
|
||||||
switch r {
|
|
||||||
case '\n':
|
|
||||||
buf = append(buf, '\\', 'n')
|
|
||||||
case '\r':
|
|
||||||
buf = append(buf, '\\', 'r')
|
|
||||||
case '\t':
|
|
||||||
buf = append(buf, '\\', 't')
|
|
||||||
case '"':
|
|
||||||
buf = append(buf, '\\', '"')
|
|
||||||
case '\\':
|
|
||||||
buf = append(buf, '\\', '\\')
|
|
||||||
case '$', '%':
|
|
||||||
buf = appendRune(buf, r)
|
|
||||||
remain := s[i+1:]
|
|
||||||
if len(remain) > 0 && remain[0] == '{' {
|
|
||||||
// Double up our template introducer symbol to escape it.
|
|
||||||
buf = appendRune(buf, r)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if !unicode.IsPrint(r) {
|
|
||||||
var fmted string
|
|
||||||
if r < 65536 {
|
|
||||||
fmted = fmt.Sprintf("\\u%04x", r)
|
|
||||||
} else {
|
|
||||||
fmted = fmt.Sprintf("\\U%08x", r)
|
|
||||||
}
|
|
||||||
buf = append(buf, fmted...)
|
|
||||||
} else {
|
|
||||||
buf = appendRune(buf, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendRune(b []byte, r rune) []byte {
|
|
||||||
l := utf8.RuneLen(r)
|
|
||||||
for i := 0; i < l; i++ {
|
|
||||||
b = append(b, 0) // make room at the end of our buffer
|
|
||||||
}
|
|
||||||
ch := b[len(b)-l:]
|
|
||||||
utf8.EncodeRune(ch, r)
|
|
||||||
return b
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
package hclwrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
||||||
)
|
|
||||||
|
|
||||||
type nativeNodeSorter struct {
|
|
||||||
Nodes []hclsyntax.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s nativeNodeSorter) Len() int {
|
|
||||||
return len(s.Nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s nativeNodeSorter) Less(i, j int) bool {
|
|
||||||
rangeI := s.Nodes[i].Range()
|
|
||||||
rangeJ := s.Nodes[j].Range()
|
|
||||||
return rangeI.Start.Byte < rangeJ.Start.Byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s nativeNodeSorter) Swap(i, j int) {
|
|
||||||
s.Nodes[i], s.Nodes[j] = s.Nodes[j], s.Nodes[i]
|
|
||||||
}
|
|
|
@ -1,236 +0,0 @@
|
||||||
package hclwrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// node represents a node in the AST.
|
|
||||||
type node struct {
|
|
||||||
content nodeContent
|
|
||||||
|
|
||||||
list *nodes
|
|
||||||
before, after *node
|
|
||||||
}
|
|
||||||
|
|
||||||
func newNode(c nodeContent) *node {
|
|
||||||
return &node{
|
|
||||||
content: c,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *node) Equal(other *node) bool {
|
|
||||||
return cmp.Equal(n.content, other.content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *node) BuildTokens(to Tokens) Tokens {
|
|
||||||
return n.content.BuildTokens(to)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detach removes the receiver from the list it currently belongs to. If the
|
|
||||||
// node is not currently in a list, this is a no-op.
|
|
||||||
func (n *node) Detach() {
|
|
||||||
if n.list == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if n.before != nil {
|
|
||||||
n.before.after = n.after
|
|
||||||
}
|
|
||||||
if n.after != nil {
|
|
||||||
n.after.before = n.before
|
|
||||||
}
|
|
||||||
if n.list.first == n {
|
|
||||||
n.list.first = n.after
|
|
||||||
}
|
|
||||||
if n.list.last == n {
|
|
||||||
n.list.last = n.before
|
|
||||||
}
|
|
||||||
n.list = nil
|
|
||||||
n.before = nil
|
|
||||||
n.after = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReplaceWith removes the receiver from the list it currently belongs to and
|
|
||||||
// inserts a new node with the given content in its place. If the node is not
|
|
||||||
// currently in a list, this function will panic.
|
|
||||||
//
|
|
||||||
// The return value is the newly-constructed node, containing the given content.
|
|
||||||
// After this function returns, the reciever is no longer attached to a list.
|
|
||||||
func (n *node) ReplaceWith(c nodeContent) *node {
|
|
||||||
if n.list == nil {
|
|
||||||
panic("can't replace node that is not in a list")
|
|
||||||
}
|
|
||||||
|
|
||||||
before := n.before
|
|
||||||
after := n.after
|
|
||||||
list := n.list
|
|
||||||
n.before, n.after, n.list = nil, nil, nil
|
|
||||||
|
|
||||||
nn := newNode(c)
|
|
||||||
nn.before = before
|
|
||||||
nn.after = after
|
|
||||||
nn.list = list
|
|
||||||
if before != nil {
|
|
||||||
before.after = nn
|
|
||||||
}
|
|
||||||
if after != nil {
|
|
||||||
after.before = nn
|
|
||||||
}
|
|
||||||
return nn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *node) assertUnattached() {
|
|
||||||
if n.list != nil {
|
|
||||||
panic(fmt.Sprintf("attempt to attach already-attached node %#v", n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodeContent is the interface type implemented by all AST content types.
|
|
||||||
type nodeContent interface {
|
|
||||||
walkChildNodes(w internalWalkFunc)
|
|
||||||
BuildTokens(to Tokens) Tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodes is a list of nodes.
|
|
||||||
type nodes struct {
|
|
||||||
first, last *node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns *nodes) BuildTokens(to Tokens) Tokens {
|
|
||||||
for n := ns.first; n != nil; n = n.after {
|
|
||||||
to = n.BuildTokens(to)
|
|
||||||
}
|
|
||||||
return to
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns *nodes) Clear() {
|
|
||||||
ns.first = nil
|
|
||||||
ns.last = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns *nodes) Append(c nodeContent) *node {
|
|
||||||
n := &node{
|
|
||||||
content: c,
|
|
||||||
}
|
|
||||||
ns.AppendNode(n)
|
|
||||||
n.list = ns
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns *nodes) AppendNode(n *node) {
|
|
||||||
if ns.last != nil {
|
|
||||||
n.before = ns.last
|
|
||||||
ns.last.after = n
|
|
||||||
}
|
|
||||||
n.list = ns
|
|
||||||
ns.last = n
|
|
||||||
if ns.first == nil {
|
|
||||||
ns.first = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns *nodes) AppendUnstructuredTokens(tokens Tokens) *node {
|
|
||||||
if len(tokens) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
n := newNode(tokens)
|
|
||||||
ns.AppendNode(n)
|
|
||||||
n.list = ns
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodeSet is an unordered set of nodes. It is used to describe a set of nodes
|
|
||||||
// that all belong to the same list that have some role or characteristic
|
|
||||||
// in common.
|
|
||||||
type nodeSet map[*node]struct{}
|
|
||||||
|
|
||||||
func newNodeSet() nodeSet {
|
|
||||||
return make(nodeSet)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns nodeSet) Has(n *node) bool {
|
|
||||||
if ns == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, exists := ns[n]
|
|
||||||
return exists
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns nodeSet) Add(n *node) {
|
|
||||||
ns[n] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns nodeSet) Remove(n *node) {
|
|
||||||
delete(ns, n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns nodeSet) List() []*node {
|
|
||||||
if len(ns) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ret := make([]*node, 0, len(ns))
|
|
||||||
|
|
||||||
// Determine which list we are working with. We assume here that all of
|
|
||||||
// the nodes belong to the same list, since that is part of the contract
|
|
||||||
// for nodeSet.
|
|
||||||
var list *nodes
|
|
||||||
for n := range ns {
|
|
||||||
list = n.list
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// We recover the order by iterating over the whole list. This is not
|
|
||||||
// the most efficient way to do it, but our node lists should always be
|
|
||||||
// small so not worth making things more complex.
|
|
||||||
for n := list.first; n != nil; n = n.after {
|
|
||||||
if ns.Has(n) {
|
|
||||||
ret = append(ret, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
type internalWalkFunc func(*node)
|
|
||||||
|
|
||||||
// inTree can be embedded into a content struct that has child nodes to get
|
|
||||||
// a standard implementation of the NodeContent interface and a record of
|
|
||||||
// a potential parent node.
|
|
||||||
type inTree struct {
|
|
||||||
parent *node
|
|
||||||
children *nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
func newInTree() inTree {
|
|
||||||
return inTree{
|
|
||||||
children: &nodes{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *inTree) assertUnattached() {
|
|
||||||
if it.parent != nil {
|
|
||||||
panic(fmt.Sprintf("node is already attached to %T", it.parent.content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *inTree) walkChildNodes(w internalWalkFunc) {
|
|
||||||
for n := it.children.first; n != nil; n = n.after {
|
|
||||||
w(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *inTree) BuildTokens(to Tokens) Tokens {
|
|
||||||
for n := it.children.first; n != nil; n = n.after {
|
|
||||||
to = n.BuildTokens(to)
|
|
||||||
}
|
|
||||||
return to
|
|
||||||
}
|
|
||||||
|
|
||||||
// leafNode can be embedded into a content struct to give it a do-nothing
|
|
||||||
// implementation of walkChildNodes
|
|
||||||
type leafNode struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *leafNode) walkChildNodes(w internalWalkFunc) {
|
|
||||||
}
|
|
|
@ -1,594 +0,0 @@
|
||||||
package hclwrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Our "parser" here is actually not doing any parsing of its own. Instead,
|
|
||||||
// it leans on the native parser in hclsyntax, and then uses the source ranges
|
|
||||||
// from the AST to partition the raw token sequence to match the raw tokens
|
|
||||||
// up to AST nodes.
|
|
||||||
//
|
|
||||||
// This strategy feels somewhat counter-intuitive, since most of the work the
|
|
||||||
// parser does is thrown away here, but this strategy is chosen because the
|
|
||||||
// normal parsing work done by hclsyntax is considered to be the "main case",
|
|
||||||
// while modifying and re-printing source is more of an edge case, used only
|
|
||||||
// in ancillary tools, and so it's good to keep all the main parsing logic
|
|
||||||
// with the main case but keep all of the extra complexity of token wrangling
|
|
||||||
// out of the main parser, which is already rather complex just serving the
|
|
||||||
// use-cases it already serves.
|
|
||||||
//
|
|
||||||
// If the parsing step produces any errors, the returned File is nil because
|
|
||||||
// we can't reliably extract tokens from the partial AST produced by an
|
|
||||||
// erroneous parse.
|
|
||||||
func parse(src []byte, filename string, start hcl.Pos) (*File, hcl.Diagnostics) {
|
|
||||||
file, diags := hclsyntax.ParseConfig(src, filename, start)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return nil, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// To do our work here, we use the "native" tokens (those from hclsyntax)
|
|
||||||
// to match against source ranges in the AST, but ultimately produce
|
|
||||||
// slices from our sequence of "writer" tokens, which contain only
|
|
||||||
// *relative* position information that is more appropriate for
|
|
||||||
// transformation/writing use-cases.
|
|
||||||
nativeTokens, diags := hclsyntax.LexConfig(src, filename, start)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
// should never happen, since we would've caught these diags in
|
|
||||||
// the first call above.
|
|
||||||
return nil, diags
|
|
||||||
}
|
|
||||||
writerTokens := writerTokens(nativeTokens)
|
|
||||||
|
|
||||||
from := inputTokens{
|
|
||||||
nativeTokens: nativeTokens,
|
|
||||||
writerTokens: writerTokens,
|
|
||||||
}
|
|
||||||
|
|
||||||
before, root, after := parseBody(file.Body.(*hclsyntax.Body), from)
|
|
||||||
ret := &File{
|
|
||||||
inTree: newInTree(),
|
|
||||||
|
|
||||||
srcBytes: src,
|
|
||||||
body: root,
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes := ret.inTree.children
|
|
||||||
nodes.Append(before.Tokens())
|
|
||||||
nodes.AppendNode(root)
|
|
||||||
nodes.Append(after.Tokens())
|
|
||||||
|
|
||||||
return ret, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
type inputTokens struct {
|
|
||||||
nativeTokens hclsyntax.Tokens
|
|
||||||
writerTokens Tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it inputTokens) Partition(rng hcl.Range) (before, within, after inputTokens) {
|
|
||||||
start, end := partitionTokens(it.nativeTokens, rng)
|
|
||||||
before = it.Slice(0, start)
|
|
||||||
within = it.Slice(start, end)
|
|
||||||
after = it.Slice(end, len(it.nativeTokens))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it inputTokens) PartitionType(ty hclsyntax.TokenType) (before, within, after inputTokens) {
|
|
||||||
for i, t := range it.writerTokens {
|
|
||||||
if t.Type == ty {
|
|
||||||
return it.Slice(0, i), it.Slice(i, i+1), it.Slice(i+1, len(it.nativeTokens))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panic(fmt.Sprintf("didn't find any token of type %s", ty))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it inputTokens) PartitionTypeSingle(ty hclsyntax.TokenType) (before inputTokens, found *Token, after inputTokens) {
|
|
||||||
before, within, after := it.PartitionType(ty)
|
|
||||||
if within.Len() != 1 {
|
|
||||||
panic("PartitionType found more than one token")
|
|
||||||
}
|
|
||||||
return before, within.Tokens()[0], after
|
|
||||||
}
|
|
||||||
|
|
||||||
// PartitionIncludeComments is like Partition except the returned "within"
|
|
||||||
// range includes any lead and line comments associated with the range.
|
|
||||||
func (it inputTokens) PartitionIncludingComments(rng hcl.Range) (before, within, after inputTokens) {
|
|
||||||
start, end := partitionTokens(it.nativeTokens, rng)
|
|
||||||
start = partitionLeadCommentTokens(it.nativeTokens[:start])
|
|
||||||
_, afterNewline := partitionLineEndTokens(it.nativeTokens[end:])
|
|
||||||
end += afterNewline
|
|
||||||
|
|
||||||
before = it.Slice(0, start)
|
|
||||||
within = it.Slice(start, end)
|
|
||||||
after = it.Slice(end, len(it.nativeTokens))
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// PartitionBlockItem is similar to PartitionIncludeComments but it returns
|
|
||||||
// the comments as separate token sequences so that they can be captured into
|
|
||||||
// AST attributes. It makes assumptions that apply only to block items, so
|
|
||||||
// should not be used for other constructs.
|
|
||||||
func (it inputTokens) PartitionBlockItem(rng hcl.Range) (before, leadComments, within, lineComments, newline, after inputTokens) {
|
|
||||||
before, within, after = it.Partition(rng)
|
|
||||||
before, leadComments = before.PartitionLeadComments()
|
|
||||||
lineComments, newline, after = after.PartitionLineEndTokens()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it inputTokens) PartitionLeadComments() (before, within inputTokens) {
|
|
||||||
start := partitionLeadCommentTokens(it.nativeTokens)
|
|
||||||
before = it.Slice(0, start)
|
|
||||||
within = it.Slice(start, len(it.nativeTokens))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it inputTokens) PartitionLineEndTokens() (comments, newline, after inputTokens) {
|
|
||||||
afterComments, afterNewline := partitionLineEndTokens(it.nativeTokens)
|
|
||||||
comments = it.Slice(0, afterComments)
|
|
||||||
newline = it.Slice(afterComments, afterNewline)
|
|
||||||
after = it.Slice(afterNewline, len(it.nativeTokens))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it inputTokens) Slice(start, end int) inputTokens {
|
|
||||||
// When we slice, we create a new slice with no additional capacity because
|
|
||||||
// we expect that these slices will be mutated in order to insert
|
|
||||||
// new code into the AST, and we want to ensure that a new underlying
|
|
||||||
// array gets allocated in that case, rather than writing into some
|
|
||||||
// following slice and corrupting it.
|
|
||||||
return inputTokens{
|
|
||||||
nativeTokens: it.nativeTokens[start:end:end],
|
|
||||||
writerTokens: it.writerTokens[start:end:end],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it inputTokens) Len() int {
|
|
||||||
return len(it.nativeTokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it inputTokens) Tokens() Tokens {
|
|
||||||
return it.writerTokens
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it inputTokens) Types() []hclsyntax.TokenType {
|
|
||||||
ret := make([]hclsyntax.TokenType, len(it.nativeTokens))
|
|
||||||
for i, tok := range it.nativeTokens {
|
|
||||||
ret[i] = tok.Type
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseBody locates the given body within the given input tokens and returns
|
|
||||||
// the resulting *Body object as well as the tokens that appeared before and
|
|
||||||
// after it.
|
|
||||||
func parseBody(nativeBody *hclsyntax.Body, from inputTokens) (inputTokens, *node, inputTokens) {
|
|
||||||
before, within, after := from.PartitionIncludingComments(nativeBody.SrcRange)
|
|
||||||
|
|
||||||
// The main AST doesn't retain the original source ordering of the
|
|
||||||
// body items, so we need to reconstruct that ordering by inspecting
|
|
||||||
// their source ranges.
|
|
||||||
nativeItems := make([]hclsyntax.Node, 0, len(nativeBody.Attributes)+len(nativeBody.Blocks))
|
|
||||||
for _, nativeAttr := range nativeBody.Attributes {
|
|
||||||
nativeItems = append(nativeItems, nativeAttr)
|
|
||||||
}
|
|
||||||
for _, nativeBlock := range nativeBody.Blocks {
|
|
||||||
nativeItems = append(nativeItems, nativeBlock)
|
|
||||||
}
|
|
||||||
sort.Sort(nativeNodeSorter{nativeItems})
|
|
||||||
|
|
||||||
body := &Body{
|
|
||||||
inTree: newInTree(),
|
|
||||||
items: newNodeSet(),
|
|
||||||
}
|
|
||||||
|
|
||||||
remain := within
|
|
||||||
for _, nativeItem := range nativeItems {
|
|
||||||
beforeItem, item, afterItem := parseBodyItem(nativeItem, remain)
|
|
||||||
|
|
||||||
if beforeItem.Len() > 0 {
|
|
||||||
body.AppendUnstructuredTokens(beforeItem.Tokens())
|
|
||||||
}
|
|
||||||
body.appendItemNode(item)
|
|
||||||
|
|
||||||
remain = afterItem
|
|
||||||
}
|
|
||||||
|
|
||||||
if remain.Len() > 0 {
|
|
||||||
body.AppendUnstructuredTokens(remain.Tokens())
|
|
||||||
}
|
|
||||||
|
|
||||||
return before, newNode(body), after
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseBodyItem(nativeItem hclsyntax.Node, from inputTokens) (inputTokens, *node, inputTokens) {
|
|
||||||
before, leadComments, within, lineComments, newline, after := from.PartitionBlockItem(nativeItem.Range())
|
|
||||||
|
|
||||||
var item *node
|
|
||||||
|
|
||||||
switch tItem := nativeItem.(type) {
|
|
||||||
case *hclsyntax.Attribute:
|
|
||||||
item = parseAttribute(tItem, within, leadComments, lineComments, newline)
|
|
||||||
case *hclsyntax.Block:
|
|
||||||
item = parseBlock(tItem, within, leadComments, lineComments, newline)
|
|
||||||
default:
|
|
||||||
// should never happen if caller is behaving
|
|
||||||
panic("unsupported native item type")
|
|
||||||
}
|
|
||||||
|
|
||||||
return before, item, after
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAttribute(nativeAttr *hclsyntax.Attribute, from, leadComments, lineComments, newline inputTokens) *node {
|
|
||||||
attr := &Attribute{
|
|
||||||
inTree: newInTree(),
|
|
||||||
}
|
|
||||||
children := attr.inTree.children
|
|
||||||
|
|
||||||
{
|
|
||||||
cn := newNode(newComments(leadComments.Tokens()))
|
|
||||||
attr.leadComments = cn
|
|
||||||
children.AppendNode(cn)
|
|
||||||
}
|
|
||||||
|
|
||||||
before, nameTokens, from := from.Partition(nativeAttr.NameRange)
|
|
||||||
{
|
|
||||||
children.AppendUnstructuredTokens(before.Tokens())
|
|
||||||
if nameTokens.Len() != 1 {
|
|
||||||
// Should never happen with valid input
|
|
||||||
panic("attribute name is not exactly one token")
|
|
||||||
}
|
|
||||||
token := nameTokens.Tokens()[0]
|
|
||||||
in := newNode(newIdentifier(token))
|
|
||||||
attr.name = in
|
|
||||||
children.AppendNode(in)
|
|
||||||
}
|
|
||||||
|
|
||||||
before, equalsTokens, from := from.Partition(nativeAttr.EqualsRange)
|
|
||||||
children.AppendUnstructuredTokens(before.Tokens())
|
|
||||||
children.AppendUnstructuredTokens(equalsTokens.Tokens())
|
|
||||||
|
|
||||||
before, exprTokens, from := from.Partition(nativeAttr.Expr.Range())
|
|
||||||
{
|
|
||||||
children.AppendUnstructuredTokens(before.Tokens())
|
|
||||||
exprNode := parseExpression(nativeAttr.Expr, exprTokens)
|
|
||||||
attr.expr = exprNode
|
|
||||||
children.AppendNode(exprNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
cn := newNode(newComments(lineComments.Tokens()))
|
|
||||||
attr.lineComments = cn
|
|
||||||
children.AppendNode(cn)
|
|
||||||
}
|
|
||||||
|
|
||||||
children.AppendUnstructuredTokens(newline.Tokens())
|
|
||||||
|
|
||||||
// Collect any stragglers, though there shouldn't be any
|
|
||||||
children.AppendUnstructuredTokens(from.Tokens())
|
|
||||||
|
|
||||||
return newNode(attr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseBlock(nativeBlock *hclsyntax.Block, from, leadComments, lineComments, newline inputTokens) *node {
|
|
||||||
block := &Block{
|
|
||||||
inTree: newInTree(),
|
|
||||||
labels: newNodeSet(),
|
|
||||||
}
|
|
||||||
children := block.inTree.children
|
|
||||||
|
|
||||||
{
|
|
||||||
cn := newNode(newComments(leadComments.Tokens()))
|
|
||||||
block.leadComments = cn
|
|
||||||
children.AppendNode(cn)
|
|
||||||
}
|
|
||||||
|
|
||||||
before, typeTokens, from := from.Partition(nativeBlock.TypeRange)
|
|
||||||
{
|
|
||||||
children.AppendUnstructuredTokens(before.Tokens())
|
|
||||||
if typeTokens.Len() != 1 {
|
|
||||||
// Should never happen with valid input
|
|
||||||
panic("block type name is not exactly one token")
|
|
||||||
}
|
|
||||||
token := typeTokens.Tokens()[0]
|
|
||||||
in := newNode(newIdentifier(token))
|
|
||||||
block.typeName = in
|
|
||||||
children.AppendNode(in)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rng := range nativeBlock.LabelRanges {
|
|
||||||
var labelTokens inputTokens
|
|
||||||
before, labelTokens, from = from.Partition(rng)
|
|
||||||
children.AppendUnstructuredTokens(before.Tokens())
|
|
||||||
tokens := labelTokens.Tokens()
|
|
||||||
ln := newNode(newQuoted(tokens))
|
|
||||||
block.labels.Add(ln)
|
|
||||||
children.AppendNode(ln)
|
|
||||||
}
|
|
||||||
|
|
||||||
before, oBrace, from := from.Partition(nativeBlock.OpenBraceRange)
|
|
||||||
children.AppendUnstructuredTokens(before.Tokens())
|
|
||||||
children.AppendUnstructuredTokens(oBrace.Tokens())
|
|
||||||
|
|
||||||
// We go a bit out of order here: we go hunting for the closing brace
|
|
||||||
// so that we have a delimited body, but then we'll deal with the body
|
|
||||||
// before we actually append the closing brace and any straggling tokens
|
|
||||||
// that appear after it.
|
|
||||||
bodyTokens, cBrace, from := from.Partition(nativeBlock.CloseBraceRange)
|
|
||||||
before, body, after := parseBody(nativeBlock.Body, bodyTokens)
|
|
||||||
children.AppendUnstructuredTokens(before.Tokens())
|
|
||||||
block.body = body
|
|
||||||
children.AppendNode(body)
|
|
||||||
children.AppendUnstructuredTokens(after.Tokens())
|
|
||||||
|
|
||||||
children.AppendUnstructuredTokens(cBrace.Tokens())
|
|
||||||
|
|
||||||
// stragglers
|
|
||||||
children.AppendUnstructuredTokens(from.Tokens())
|
|
||||||
if lineComments.Len() > 0 {
|
|
||||||
// blocks don't actually have line comments, so we'll just treat
|
|
||||||
// them as extra stragglers
|
|
||||||
children.AppendUnstructuredTokens(lineComments.Tokens())
|
|
||||||
}
|
|
||||||
children.AppendUnstructuredTokens(newline.Tokens())
|
|
||||||
|
|
||||||
return newNode(block)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseExpression(nativeExpr hclsyntax.Expression, from inputTokens) *node {
|
|
||||||
expr := newExpression()
|
|
||||||
children := expr.inTree.children
|
|
||||||
|
|
||||||
nativeVars := nativeExpr.Variables()
|
|
||||||
|
|
||||||
for _, nativeTraversal := range nativeVars {
|
|
||||||
before, traversal, after := parseTraversal(nativeTraversal, from)
|
|
||||||
children.AppendUnstructuredTokens(before.Tokens())
|
|
||||||
children.AppendNode(traversal)
|
|
||||||
expr.absTraversals.Add(traversal)
|
|
||||||
from = after
|
|
||||||
}
|
|
||||||
// Attach any stragglers that don't belong to a traversal to the expression
|
|
||||||
// itself. In an expression with no traversals at all, this is just the
|
|
||||||
// entirety of "from".
|
|
||||||
children.AppendUnstructuredTokens(from.Tokens())
|
|
||||||
|
|
||||||
return newNode(expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseTraversal(nativeTraversal hcl.Traversal, from inputTokens) (before inputTokens, n *node, after inputTokens) {
|
|
||||||
traversal := newTraversal()
|
|
||||||
children := traversal.inTree.children
|
|
||||||
before, from, after = from.Partition(nativeTraversal.SourceRange())
|
|
||||||
|
|
||||||
stepAfter := from
|
|
||||||
for _, nativeStep := range nativeTraversal {
|
|
||||||
before, step, after := parseTraversalStep(nativeStep, stepAfter)
|
|
||||||
children.AppendUnstructuredTokens(before.Tokens())
|
|
||||||
children.AppendNode(step)
|
|
||||||
traversal.steps.Add(step)
|
|
||||||
stepAfter = after
|
|
||||||
}
|
|
||||||
|
|
||||||
return before, newNode(traversal), after
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseTraversalStep(nativeStep hcl.Traverser, from inputTokens) (before inputTokens, n *node, after inputTokens) {
|
|
||||||
var children *nodes
|
|
||||||
switch tNativeStep := nativeStep.(type) {
|
|
||||||
|
|
||||||
case hcl.TraverseRoot, hcl.TraverseAttr:
|
|
||||||
step := newTraverseName()
|
|
||||||
children = step.inTree.children
|
|
||||||
before, from, after = from.Partition(nativeStep.SourceRange())
|
|
||||||
inBefore, token, inAfter := from.PartitionTypeSingle(hclsyntax.TokenIdent)
|
|
||||||
name := newIdentifier(token)
|
|
||||||
children.AppendUnstructuredTokens(inBefore.Tokens())
|
|
||||||
step.name = children.Append(name)
|
|
||||||
children.AppendUnstructuredTokens(inAfter.Tokens())
|
|
||||||
return before, newNode(step), after
|
|
||||||
|
|
||||||
case hcl.TraverseIndex:
|
|
||||||
step := newTraverseIndex()
|
|
||||||
children = step.inTree.children
|
|
||||||
before, from, after = from.Partition(nativeStep.SourceRange())
|
|
||||||
|
|
||||||
var inBefore, oBrack, keyTokens, cBrack inputTokens
|
|
||||||
inBefore, oBrack, from = from.PartitionType(hclsyntax.TokenOBrack)
|
|
||||||
children.AppendUnstructuredTokens(inBefore.Tokens())
|
|
||||||
children.AppendUnstructuredTokens(oBrack.Tokens())
|
|
||||||
keyTokens, cBrack, from = from.PartitionType(hclsyntax.TokenCBrack)
|
|
||||||
|
|
||||||
keyVal := tNativeStep.Key
|
|
||||||
switch keyVal.Type() {
|
|
||||||
case cty.String:
|
|
||||||
key := newQuoted(keyTokens.Tokens())
|
|
||||||
step.key = children.Append(key)
|
|
||||||
case cty.Number:
|
|
||||||
valBefore, valToken, valAfter := keyTokens.PartitionTypeSingle(hclsyntax.TokenNumberLit)
|
|
||||||
children.AppendUnstructuredTokens(valBefore.Tokens())
|
|
||||||
key := newNumber(valToken)
|
|
||||||
step.key = children.Append(key)
|
|
||||||
children.AppendUnstructuredTokens(valAfter.Tokens())
|
|
||||||
}
|
|
||||||
|
|
||||||
children.AppendUnstructuredTokens(cBrack.Tokens())
|
|
||||||
children.AppendUnstructuredTokens(from.Tokens())
|
|
||||||
|
|
||||||
return before, newNode(step), after
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unsupported traversal step type %T", nativeStep))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// writerTokens takes a sequence of tokens as produced by the main hclsyntax
|
|
||||||
// package and transforms it into an equivalent sequence of tokens using
|
|
||||||
// this package's own token model.
|
|
||||||
//
|
|
||||||
// The resulting list contains the same number of tokens and uses the same
|
|
||||||
// indices as the input, allowing the two sets of tokens to be correlated
|
|
||||||
// by index.
|
|
||||||
func writerTokens(nativeTokens hclsyntax.Tokens) Tokens {
|
|
||||||
// Ultimately we want a slice of token _pointers_, but since we can
|
|
||||||
// predict how much memory we're going to devote to tokens we'll allocate
|
|
||||||
// it all as a single flat buffer and thus give the GC less work to do.
|
|
||||||
tokBuf := make([]Token, len(nativeTokens))
|
|
||||||
var lastByteOffset int
|
|
||||||
for i, mainToken := range nativeTokens {
|
|
||||||
// Create a copy of the bytes so that we can mutate without
|
|
||||||
// corrupting the original token stream.
|
|
||||||
bytes := make([]byte, len(mainToken.Bytes))
|
|
||||||
copy(bytes, mainToken.Bytes)
|
|
||||||
|
|
||||||
tokBuf[i] = Token{
|
|
||||||
Type: mainToken.Type,
|
|
||||||
Bytes: bytes,
|
|
||||||
|
|
||||||
// We assume here that spaces are always ASCII spaces, since
|
|
||||||
// that's what the scanner also assumes, and thus the number
|
|
||||||
// of bytes skipped is also the number of space characters.
|
|
||||||
SpacesBefore: mainToken.Range.Start.Byte - lastByteOffset,
|
|
||||||
}
|
|
||||||
|
|
||||||
lastByteOffset = mainToken.Range.End.Byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now make a slice of pointers into the previous slice.
|
|
||||||
ret := make(Tokens, len(tokBuf))
|
|
||||||
for i := range ret {
|
|
||||||
ret[i] = &tokBuf[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// partitionTokens takes a sequence of tokens and a hcl.Range and returns
|
|
||||||
// two indices within the token sequence that correspond with the range
|
|
||||||
// boundaries, such that the slice operator could be used to produce
|
|
||||||
// three token sequences for before, within, and after respectively:
|
|
||||||
//
|
|
||||||
// start, end := partitionTokens(toks, rng)
|
|
||||||
// before := toks[:start]
|
|
||||||
// within := toks[start:end]
|
|
||||||
// after := toks[end:]
|
|
||||||
//
|
|
||||||
// This works best when the range is aligned with token boundaries (e.g.
|
|
||||||
// because it was produced in terms of the scanner's result) but if that isn't
|
|
||||||
// true then it will make a best effort that may produce strange results at
|
|
||||||
// the boundaries.
|
|
||||||
//
|
|
||||||
// Native hclsyntax tokens are used here, because they contain the necessary
|
|
||||||
// absolute position information. However, since writerTokens produces a
|
|
||||||
// correlatable sequence of writer tokens, the resulting indices can be
|
|
||||||
// used also to index into its result, allowing the partitioning of writer
|
|
||||||
// tokens to be driven by the partitioning of native tokens.
|
|
||||||
//
|
|
||||||
// The tokens are assumed to be in source order and non-overlapping, which
|
|
||||||
// will be true if the token sequence from the scanner is used directly.
|
|
||||||
func partitionTokens(toks hclsyntax.Tokens, rng hcl.Range) (start, end int) {
|
|
||||||
// We us a linear search here because we assume tha in most cases our
|
|
||||||
// target range is close to the beginning of the sequence, and the seqences
|
|
||||||
// are generally small for most reasonable files anyway.
|
|
||||||
for i := 0; ; i++ {
|
|
||||||
if i >= len(toks) {
|
|
||||||
// No tokens for the given range at all!
|
|
||||||
return len(toks), len(toks)
|
|
||||||
}
|
|
||||||
|
|
||||||
if toks[i].Range.Start.Byte >= rng.Start.Byte {
|
|
||||||
start = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := start; ; i++ {
|
|
||||||
if i >= len(toks) {
|
|
||||||
// The range "hangs off" the end of the token sequence
|
|
||||||
return start, len(toks)
|
|
||||||
}
|
|
||||||
|
|
||||||
if toks[i].Range.Start.Byte >= rng.End.Byte {
|
|
||||||
end = i // end marker is exclusive
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return start, end
|
|
||||||
}
|
|
||||||
|
|
||||||
// partitionLeadCommentTokens takes a sequence of tokens that is assumed
|
|
||||||
// to immediately precede a construct that can have lead comment tokens,
|
|
||||||
// and returns the index into that sequence where the lead comments begin.
|
|
||||||
//
|
|
||||||
// Lead comments are defined as whole lines containing only comment tokens
|
|
||||||
// with no blank lines between. If no such lines are found, the returned
|
|
||||||
// index will be len(toks).
|
|
||||||
func partitionLeadCommentTokens(toks hclsyntax.Tokens) int {
|
|
||||||
// single-line comments (which is what we're interested in here)
|
|
||||||
// consume their trailing newline, so we can just walk backwards
|
|
||||||
// until we stop seeing comment tokens.
|
|
||||||
for i := len(toks) - 1; i >= 0; i-- {
|
|
||||||
if toks[i].Type != hclsyntax.TokenComment {
|
|
||||||
return i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// partitionLineEndTokens takes a sequence of tokens that is assumed
|
|
||||||
// to immediately follow a construct that can have a line comment, and
|
|
||||||
// returns first the index where any line comments end and then second
|
|
||||||
// the index immediately after the trailing newline.
|
|
||||||
//
|
|
||||||
// Line comments are defined as comments that appear immediately after
|
|
||||||
// a construct on the same line where its significant tokens ended.
|
|
||||||
//
|
|
||||||
// Since single-line comment tokens (# and //) include the newline that
|
|
||||||
// terminates them, in the presence of these the two returned indices
|
|
||||||
// will be the same since the comment itself serves as the line end.
|
|
||||||
func partitionLineEndTokens(toks hclsyntax.Tokens) (afterComment, afterNewline int) {
|
|
||||||
for i := 0; i < len(toks); i++ {
|
|
||||||
tok := toks[i]
|
|
||||||
if tok.Type != hclsyntax.TokenComment {
|
|
||||||
switch tok.Type {
|
|
||||||
case hclsyntax.TokenNewline:
|
|
||||||
return i, i + 1
|
|
||||||
case hclsyntax.TokenEOF:
|
|
||||||
// Although this is valid, we mustn't include the EOF
|
|
||||||
// itself as our "newline" or else strange things will
|
|
||||||
// happen when we try to append new items.
|
|
||||||
return i, i
|
|
||||||
default:
|
|
||||||
// If we have well-formed input here then nothing else should be
|
|
||||||
// possible. This path should never happen, because we only try
|
|
||||||
// to extract tokens from the sequence if the parser succeeded,
|
|
||||||
// and it should catch this problem itself.
|
|
||||||
panic("malformed line trailers: expected only comments and newlines")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tok.Bytes) > 0 && tok.Bytes[len(tok.Bytes)-1] == '\n' {
|
|
||||||
// Newline at the end of a single-line comment serves both as
|
|
||||||
// the end of comments *and* the end of the line.
|
|
||||||
return i + 1, i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(toks), len(toks)
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexConfig uses the hclsyntax scanner to get a token stream and then
|
|
||||||
// rewrites it into this package's token model.
|
|
||||||
//
|
|
||||||
// Any errors produced during scanning are ignored, so the results of this
|
|
||||||
// function should be used with care.
|
|
||||||
func lexConfig(src []byte) Tokens {
|
|
||||||
mainTokens, _ := hclsyntax.LexConfig(src, "", hcl.Pos{Byte: 0, Line: 1, Column: 1})
|
|
||||||
return writerTokens(mainTokens)
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package hclwrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewFile creates a new file object that is empty and ready to have constructs
|
|
||||||
// added t it.
|
|
||||||
func NewFile() *File {
|
|
||||||
body := &Body{
|
|
||||||
inTree: newInTree(),
|
|
||||||
items: newNodeSet(),
|
|
||||||
}
|
|
||||||
file := &File{
|
|
||||||
inTree: newInTree(),
|
|
||||||
}
|
|
||||||
file.body = file.inTree.children.Append(body)
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseConfig interprets the given source bytes into a *hclwrite.File. The
|
|
||||||
// resulting AST can be used to perform surgical edits on the source code
|
|
||||||
// before turning it back into bytes again.
|
|
||||||
func ParseConfig(src []byte, filename string, start hcl.Pos) (*File, hcl.Diagnostics) {
|
|
||||||
return parse(src, filename, start)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format takes source code and performs simple whitespace changes to transform
|
|
||||||
// it to a canonical layout style.
|
|
||||||
//
|
|
||||||
// Format skips constructing an AST and works directly with tokens, so it
|
|
||||||
// is less expensive than formatting via the AST for situations where no other
|
|
||||||
// changes will be made. It also ignores syntax errors and can thus be applied
|
|
||||||
// to partial source code, although the result in that case may not be
|
|
||||||
// desirable.
|
|
||||||
func Format(src []byte) []byte {
|
|
||||||
tokens := lexConfig(src)
|
|
||||||
format(tokens)
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
tokens.WriteTo(buf)
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
package hclwrite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/apparentlymart/go-textseg/textseg"
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Token is a single sequence of bytes annotated with a type. It is similar
|
|
||||||
// in purpose to hclsyntax.Token, but discards the source position information
|
|
||||||
// since that is not useful in code generation.
|
|
||||||
type Token struct {
|
|
||||||
Type hclsyntax.TokenType
|
|
||||||
Bytes []byte
|
|
||||||
|
|
||||||
// We record the number of spaces before each token so that we can
|
|
||||||
// reproduce the exact layout of the original file when we're making
|
|
||||||
// surgical changes in-place. When _new_ code is created it will always
|
|
||||||
// be in the canonical style, but we preserve layout of existing code.
|
|
||||||
SpacesBefore int
|
|
||||||
}
|
|
||||||
|
|
||||||
// asHCLSyntax returns the receiver expressed as an incomplete hclsyntax.Token.
|
|
||||||
// A complete token is not possible since we don't have source location
|
|
||||||
// information here, and so this method is unexported so we can be sure it will
|
|
||||||
// only be used for internal purposes where we know the range isn't important.
|
|
||||||
//
|
|
||||||
// This is primarily intended to allow us to re-use certain functionality from
|
|
||||||
// hclsyntax rather than re-implementing it against our own token type here.
|
|
||||||
func (t *Token) asHCLSyntax() hclsyntax.Token {
|
|
||||||
return hclsyntax.Token{
|
|
||||||
Type: t.Type,
|
|
||||||
Bytes: t.Bytes,
|
|
||||||
Range: hcl.Range{
|
|
||||||
Filename: "<invalid>",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tokens is a flat list of tokens.
|
|
||||||
type Tokens []*Token
|
|
||||||
|
|
||||||
func (ts Tokens) Bytes() []byte {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
ts.WriteTo(buf)
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts Tokens) testValue() string {
|
|
||||||
return string(ts.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Columns returns the number of columns (grapheme clusters) the token sequence
|
|
||||||
// occupies. The result is not meaningful if there are newline or single-line
|
|
||||||
// comment tokens in the sequence.
|
|
||||||
func (ts Tokens) Columns() int {
|
|
||||||
ret := 0
|
|
||||||
for _, token := range ts {
|
|
||||||
ret += token.SpacesBefore // spaces are always worth one column each
|
|
||||||
ct, _ := textseg.TokenCount(token.Bytes, textseg.ScanGraphemeClusters)
|
|
||||||
ret += ct
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTo takes an io.Writer and writes the bytes for each token to it,
|
|
||||||
// along with the spacing that separates each token. In other words, this
|
|
||||||
// allows serializing the tokens to a file or other such byte stream.
|
|
||||||
func (ts Tokens) WriteTo(wr io.Writer) (int64, error) {
|
|
||||||
// We know we're going to be writing a lot of small chunks of repeated
|
|
||||||
// space characters, so we'll prepare a buffer of these that we can
|
|
||||||
// easily pass to wr.Write without any further allocation.
|
|
||||||
spaces := make([]byte, 40)
|
|
||||||
for i := range spaces {
|
|
||||||
spaces[i] = ' '
|
|
||||||
}
|
|
||||||
|
|
||||||
var n int64
|
|
||||||
var err error
|
|
||||||
for _, token := range ts {
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for spacesBefore := token.SpacesBefore; spacesBefore > 0; spacesBefore -= len(spaces) {
|
|
||||||
thisChunk := spacesBefore
|
|
||||||
if thisChunk > len(spaces) {
|
|
||||||
thisChunk = len(spaces)
|
|
||||||
}
|
|
||||||
var thisN int
|
|
||||||
thisN, err = wr.Write(spaces[:thisChunk])
|
|
||||||
n += int64(thisN)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var thisN int
|
|
||||||
thisN, err = wr.Write(token.Bytes)
|
|
||||||
n += int64(thisN)
|
|
||||||
}
|
|
||||||
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts Tokens) walkChildNodes(w internalWalkFunc) {
|
|
||||||
// Unstructured tokens have no child nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts Tokens) BuildTokens(to Tokens) Tokens {
|
|
||||||
return append(to, ts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newIdentToken(name string) *Token {
|
|
||||||
return &Token{
|
|
||||||
Type: hclsyntax.TokenIdent,
|
|
||||||
Bytes: []byte(name),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
legacyhclparser "github.com/hashicorp/hcl/hcl/parser"
|
legacyhclparser "github.com/hashicorp/hcl/hcl/parser"
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
"github.com/hashicorp/hcl/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Diagnostic describes a problem (error or warning) encountered during
|
// Diagnostic describes a problem (error or warning) encountered during
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
"github.com/hashicorp/hcl/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoadModule reads the directory at the given path and attempts to interpret
|
// LoadModule reads the directory at the given path and attempts to interpret
|
||||||
|
@ -52,12 +52,12 @@ func (m *Module) init(diags Diagnostics) {
|
||||||
// case so callers can easily recognize it.
|
// case so callers can easily recognize it.
|
||||||
for _, r := range m.ManagedResources {
|
for _, r := range m.ManagedResources {
|
||||||
if _, exists := m.RequiredProviders[r.Provider.Name]; !exists {
|
if _, exists := m.RequiredProviders[r.Provider.Name]; !exists {
|
||||||
m.RequiredProviders[r.Provider.Name] = []string{}
|
m.RequiredProviders[r.Provider.Name] = &ProviderRequirement{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, r := range m.DataResources {
|
for _, r := range m.DataResources {
|
||||||
if _, exists := m.RequiredProviders[r.Provider.Name]; !exists {
|
if _, exists := m.RequiredProviders[r.Provider.Name]; !exists {
|
||||||
m.RequiredProviders[r.Provider.Name] = []string{}
|
m.RequiredProviders[r.Provider.Name] = &ProviderRequirement{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl2/gohcl"
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
"github.com/hashicorp/hcl/v2/gohcl"
|
||||||
"github.com/hashicorp/hcl2/hclparse"
|
"github.com/hashicorp/hcl/v2/hclparse"
|
||||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,18 +51,17 @@ func loadModule(dir string) (*Module, Diagnostics) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, block := range content.Blocks {
|
for _, innerBlock := range content.Blocks {
|
||||||
// Our schema only allows required_providers here, so we
|
switch innerBlock.Type {
|
||||||
// assume that we'll only get that block type.
|
case "required_providers":
|
||||||
attrs, attrDiags := block.Body.JustAttributes()
|
reqs, reqsDiags := decodeRequiredProvidersBlock(innerBlock)
|
||||||
diags = append(diags, attrDiags...)
|
diags = append(diags, reqsDiags...)
|
||||||
|
for name, req := range reqs {
|
||||||
for name, attr := range attrs {
|
if _, exists := mod.RequiredProviders[name]; !exists {
|
||||||
var version string
|
mod.RequiredProviders[name] = req
|
||||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &version)
|
} else {
|
||||||
diags = append(diags, valDiags...)
|
mod.RequiredProviders[name].VersionConstraints = append(mod.RequiredProviders[name].VersionConstraints, req.VersionConstraints...)
|
||||||
if !valDiags.HasErrors() {
|
}
|
||||||
mod.RequiredProviders[name] = append(mod.RequiredProviders[name], version)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,22 +177,20 @@ func loadModule(dir string) (*Module, Diagnostics) {
|
||||||
diags = append(diags, contentDiags...)
|
diags = append(diags, contentDiags...)
|
||||||
|
|
||||||
name := block.Labels[0]
|
name := block.Labels[0]
|
||||||
|
// Even if there isn't an explicit version required, we still
|
||||||
|
// need an entry in our map to signal the unversioned dependency.
|
||||||
|
if _, exists := mod.RequiredProviders[name]; !exists {
|
||||||
|
mod.RequiredProviders[name] = &ProviderRequirement{}
|
||||||
|
}
|
||||||
if attr, defined := content.Attributes["version"]; defined {
|
if attr, defined := content.Attributes["version"]; defined {
|
||||||
var version string
|
var version string
|
||||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &version)
|
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &version)
|
||||||
diags = append(diags, valDiags...)
|
diags = append(diags, valDiags...)
|
||||||
if !valDiags.HasErrors() {
|
if !valDiags.HasErrors() {
|
||||||
mod.RequiredProviders[name] = append(mod.RequiredProviders[name], version)
|
mod.RequiredProviders[name].VersionConstraints = append(mod.RequiredProviders[name].VersionConstraints, version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Even if there wasn't an explicit version required, we still
|
|
||||||
// need an entry in our map to signal the unversioned dependency.
|
|
||||||
if _, exists := mod.RequiredProviders[name]; !exists {
|
|
||||||
mod.RequiredProviders[name] = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "resource", "data":
|
case "resource", "data":
|
||||||
|
|
||||||
content, _, contentDiags := block.Body.PartialContent(resourceSchema)
|
content, _, contentDiags := block.Body.PartialContent(resourceSchema)
|
||||||
|
|
|
@ -267,17 +267,15 @@ func loadModuleLegacyHCL(dir string) (*Module, Diagnostics) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, diagnosticsErrorf("invalid provider block at %s: %s", item.Pos(), err)
|
return nil, diagnosticsErrorf("invalid provider block at %s: %s", item.Pos(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if block.Version != "" {
|
|
||||||
mod.RequiredProviders[name] = append(mod.RequiredProviders[name], block.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Even if there wasn't an explicit version required, we still
|
// Even if there wasn't an explicit version required, we still
|
||||||
// need an entry in our map to signal the unversioned dependency.
|
// need an entry in our map to signal the unversioned dependency.
|
||||||
if _, exists := mod.RequiredProviders[name]; !exists {
|
if _, exists := mod.RequiredProviders[name]; !exists {
|
||||||
mod.RequiredProviders[name] = []string{}
|
mod.RequiredProviders[name] = &ProviderRequirement{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if block.Version != "" {
|
||||||
|
mod.RequiredProviders[name].VersionConstraints = append(mod.RequiredProviders[name].VersionConstraints, block.Version)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ type Module struct {
|
||||||
Outputs map[string]*Output `json:"outputs"`
|
Outputs map[string]*Output `json:"outputs"`
|
||||||
|
|
||||||
RequiredCore []string `json:"required_core,omitempty"`
|
RequiredCore []string `json:"required_core,omitempty"`
|
||||||
RequiredProviders map[string][]string `json:"required_providers"`
|
RequiredProviders map[string]*ProviderRequirement `json:"required_providers"`
|
||||||
|
|
||||||
ManagedResources map[string]*Resource `json:"managed_resources"`
|
ManagedResources map[string]*Resource `json:"managed_resources"`
|
||||||
DataResources map[string]*Resource `json:"data_resources"`
|
DataResources map[string]*Resource `json:"data_resources"`
|
||||||
|
@ -27,7 +27,7 @@ func newModule(path string) *Module {
|
||||||
Path: path,
|
Path: path,
|
||||||
Variables: make(map[string]*Variable),
|
Variables: make(map[string]*Variable),
|
||||||
Outputs: make(map[string]*Output),
|
Outputs: make(map[string]*Output),
|
||||||
RequiredProviders: make(map[string][]string),
|
RequiredProviders: make(map[string]*ProviderRequirement),
|
||||||
ManagedResources: make(map[string]*Resource),
|
ManagedResources: make(map[string]*Resource),
|
||||||
DataResources: make(map[string]*Resource),
|
DataResources: make(map[string]*Resource),
|
||||||
ModuleCalls: make(map[string]*ModuleCall),
|
ModuleCalls: make(map[string]*ModuleCall),
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
package tfconfig
|
package tfconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/gohcl"
|
||||||
|
"github.com/zclconf/go-cty/cty/gocty"
|
||||||
|
)
|
||||||
|
|
||||||
// ProviderRef is a reference to a provider configuration within a module.
|
// ProviderRef is a reference to a provider configuration within a module.
|
||||||
// It represents the contents of a "provider" argument in a resource, or
|
// It represents the contents of a "provider" argument in a resource, or
|
||||||
// a value in the "providers" map for a module call.
|
// a value in the "providers" map for a module call.
|
||||||
|
@ -7,3 +13,73 @@ type ProviderRef struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Alias string `json:"alias,omitempty"` // Empty if the default provider configuration is referenced
|
Alias string `json:"alias,omitempty"` // Empty if the default provider configuration is referenced
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProviderRequirement struct {
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
VersionConstraints []string `json:"version_constraints,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeRequiredProvidersBlock(block *hcl.Block) (map[string]*ProviderRequirement, hcl.Diagnostics) {
|
||||||
|
attrs, diags := block.Body.JustAttributes()
|
||||||
|
reqs := make(map[string]*ProviderRequirement)
|
||||||
|
for name, attr := range attrs {
|
||||||
|
expr, err := attr.Expr.Value(nil)
|
||||||
|
if err != nil {
|
||||||
|
diags = append(diags, err...)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case expr.Type().IsPrimitiveType():
|
||||||
|
var version string
|
||||||
|
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &version)
|
||||||
|
diags = append(diags, valDiags...)
|
||||||
|
if !valDiags.HasErrors() {
|
||||||
|
reqs[name] = &ProviderRequirement{
|
||||||
|
VersionConstraints: []string{version},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case expr.Type().IsObjectType():
|
||||||
|
var pr ProviderRequirement
|
||||||
|
if expr.Type().HasAttribute("version") {
|
||||||
|
var version string
|
||||||
|
err := gocty.FromCtyValue(expr.GetAttr("version"), &version)
|
||||||
|
if err == nil {
|
||||||
|
pr.VersionConstraints = append(pr.VersionConstraints, version)
|
||||||
|
} else {
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Unsuitable value type",
|
||||||
|
Detail: "Unsuitable value: string required",
|
||||||
|
Subject: attr.Expr.Range().Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if expr.Type().HasAttribute("source") {
|
||||||
|
var source string
|
||||||
|
err := gocty.FromCtyValue(expr.GetAttr("source"), &source)
|
||||||
|
if err == nil {
|
||||||
|
pr.Source = source
|
||||||
|
} else {
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Unsuitable value type",
|
||||||
|
Detail: "Unsuitable value: string required",
|
||||||
|
Subject: attr.Expr.Range().Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reqs[name] = &pr
|
||||||
|
|
||||||
|
default:
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Unsuitable value type",
|
||||||
|
Detail: "Unsuitable value: string required",
|
||||||
|
Subject: attr.Expr.Range().Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reqs, diags
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package tfconfig
|
package tfconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
"github.com/hashicorp/hcl/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootSchema = &hcl.BodySchema{
|
var rootSchema = &hcl.BodySchema{
|
||||||
|
|
|
@ -2,7 +2,7 @@ package tfconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
legacyhcltoken "github.com/hashicorp/hcl/hcl/token"
|
legacyhcltoken "github.com/hashicorp/hcl/hcl/token"
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
"github.com/hashicorp/hcl/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SourcePos is a pointer to a particular location in a source file.
|
// SourcePos is a pointer to a particular location in a source file.
|
||||||
|
|
|
@ -358,13 +358,6 @@ github.com/hashicorp/hcl/v2/gohcl
|
||||||
github.com/hashicorp/hcl/v2/ext/typeexpr
|
github.com/hashicorp/hcl/v2/ext/typeexpr
|
||||||
github.com/hashicorp/hcl/v2/ext/dynblock
|
github.com/hashicorp/hcl/v2/ext/dynblock
|
||||||
github.com/hashicorp/hcl/v2/hcltest
|
github.com/hashicorp/hcl/v2/hcltest
|
||||||
# github.com/hashicorp/hcl2 v0.0.0-20190821123243-0c888d1241f6
|
|
||||||
github.com/hashicorp/hcl2/gohcl
|
|
||||||
github.com/hashicorp/hcl2/hcl
|
|
||||||
github.com/hashicorp/hcl2/hcl/hclsyntax
|
|
||||||
github.com/hashicorp/hcl2/hclparse
|
|
||||||
github.com/hashicorp/hcl2/hclwrite
|
|
||||||
github.com/hashicorp/hcl2/hcl/json
|
|
||||||
# github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590
|
# github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590
|
||||||
github.com/hashicorp/hil
|
github.com/hashicorp/hil
|
||||||
github.com/hashicorp/hil/ast
|
github.com/hashicorp/hil/ast
|
||||||
|
@ -372,7 +365,7 @@ github.com/hashicorp/hil/parser
|
||||||
github.com/hashicorp/hil/scanner
|
github.com/hashicorp/hil/scanner
|
||||||
# github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb
|
# github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb
|
||||||
github.com/hashicorp/serf/coordinate
|
github.com/hashicorp/serf/coordinate
|
||||||
# github.com/hashicorp/terraform-config-inspect v0.0.0-20190821133035-82a99dc22ef4
|
# github.com/hashicorp/terraform-config-inspect v0.0.0-20191212124732-c6ae6269b9d7
|
||||||
github.com/hashicorp/terraform-config-inspect/tfconfig
|
github.com/hashicorp/terraform-config-inspect/tfconfig
|
||||||
# github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596
|
# github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596
|
||||||
github.com/hashicorp/terraform-svchost
|
github.com/hashicorp/terraform-svchost
|
||||||
|
|
Loading…
Reference in New Issue