├── .gitignore ├── tools └── tools.go ├── tflint ├── text_node.go ├── config.go ├── doc.go ├── issue.go ├── expandmode_string.go ├── walker.go ├── modulectxtype_string.go ├── rule.go ├── errors.go ├── option.go ├── ruleset_test.go ├── ruleset.go └── interface.go ├── terraform ├── config.go ├── addrs │ ├── doc.go │ ├── local_value.go │ ├── input_variable.go │ ├── path_attr.go │ ├── count_attr.go │ ├── self.go │ ├── terraform_attr.go │ ├── for_each_attr.go │ ├── referenceable.go │ ├── module.go │ ├── resourcemode_string.go │ ├── module_call.go │ ├── resource.go │ ├── instance_key.go │ └── parse_ref.go ├── README.md └── lang │ ├── doc.go │ ├── marks │ └── marks.go │ ├── references.go │ └── references_test.go ├── plugin ├── internal │ ├── interceptor │ │ ├── doc.go │ │ └── logging.go │ ├── proto │ │ ├── doc.go │ │ └── tflint.proto │ ├── toproto │ │ ├── doc.go │ │ └── toproto.go │ ├── fromproto │ │ ├── doc.go │ │ └── fromproto.go │ ├── plugin2host │ │ ├── doc.go │ │ └── server.go │ └── host2plugin │ │ ├── doc.go │ │ ├── plugin.go │ │ ├── client.go │ │ └── server.go ├── plugin2host │ ├── plugin2host.go │ └── doc.go ├── doc.go ├── host2plugin │ ├── doc.go │ └── host2plugin.go └── plugin.go ├── .github ├── dependabot.yml └── workflows │ ├── dependabot.yml │ ├── build.yml │ └── lint.yml ├── logger ├── doc.go └── logger.go ├── helper ├── doc.go ├── issue.go ├── testing.go └── runner.go ├── Makefile ├── aqua.yaml ├── hclext ├── schemamode_string.go ├── doc.go ├── expression.go ├── parse.go ├── decode_example_test.go ├── parse_test.go ├── structure_example_test.go ├── schema_test.go ├── schema.go ├── decode.go └── structure.go ├── go.mod ├── README.md ├── internal ├── token_scanner.go └── token_scanner_test.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | /vendor 3 | /dist 4 | tflint-ruleset-* 5 | .vscode -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "golang.org/x/tools/cmd/stringer" 7 | ) 8 | -------------------------------------------------------------------------------- /tflint/text_node.go: -------------------------------------------------------------------------------- 1 | package tflint 2 | 3 | import "github.com/hashicorp/hcl/v2" 4 | 5 | // TextNode represents a text with range in the source code. 6 | type TextNode struct { 7 | Bytes []byte 8 | Range hcl.Range 9 | } 10 | -------------------------------------------------------------------------------- /terraform/config.go: -------------------------------------------------------------------------------- 1 | package terraform 2 | 3 | import "strings" 4 | 5 | // IsJSONFilename returns true if the filename is a JSON syntax file. 6 | func IsJSONFilename(filename string) bool { 7 | return strings.HasSuffix(filename, ".tf.json") 8 | } 9 | -------------------------------------------------------------------------------- /plugin/internal/interceptor/doc.go: -------------------------------------------------------------------------------- 1 | // Package interceptor contains gRPC interceptors. 2 | // This package is not intended to be used directly from plugins. 3 | // Its main use today is to insert shared processes such as logging. 4 | package interceptor 5 | -------------------------------------------------------------------------------- /terraform/addrs/doc.go: -------------------------------------------------------------------------------- 1 | // Package addrs is a fork of Terraform's internal/addrs package. 2 | // 3 | // This package contains reference addresses in the Terraform Language 4 | // and implementations that parse hcl.Traversal to produce references. 5 | package addrs 6 | -------------------------------------------------------------------------------- /terraform/addrs/local_value.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | // LocalValue is the address of a local value. 4 | type LocalValue struct { 5 | referenceable 6 | Name string 7 | } 8 | 9 | func (v LocalValue) String() string { 10 | return "local." + v.Name 11 | } 12 | -------------------------------------------------------------------------------- /terraform/addrs/input_variable.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | // InputVariable is the address of an input variable. 4 | type InputVariable struct { 5 | referenceable 6 | Name string 7 | } 8 | 9 | func (v InputVariable) String() string { 10 | return "var." + v.Name 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | -------------------------------------------------------------------------------- /plugin/plugin2host/plugin2host.go: -------------------------------------------------------------------------------- 1 | package plugin2host 2 | 3 | import "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/plugin2host" 4 | 5 | // Server is the interface that the host should implement when a plugin communicates with the host. 6 | type Server = plugin2host.Server 7 | -------------------------------------------------------------------------------- /terraform/addrs/path_attr.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | // PathAttr is the address of an attribute of the "path" object in 4 | // the interpolation scope, like "path.module". 5 | type PathAttr struct { 6 | referenceable 7 | Name string 8 | } 9 | 10 | func (pa PathAttr) String() string { 11 | return "path." + pa.Name 12 | } 13 | -------------------------------------------------------------------------------- /plugin/doc.go: -------------------------------------------------------------------------------- 1 | // Package plugin contains the implementations needed to make 2 | // the built binary act as a plugin. 3 | // 4 | // A plugin is implemented as an gRPC server and the host acts 5 | // as the client, sending analysis requests to the plugin. 6 | // 7 | // See internal/host2plugin for implementation details. 8 | package plugin 9 | -------------------------------------------------------------------------------- /plugin/internal/proto/doc.go: -------------------------------------------------------------------------------- 1 | // Package proto contains generated protocol buffers structures. 2 | // This package is not intended to be used directly from plugins. 3 | // 4 | // Do not include anything other than automatically generated ones here. 5 | // If you want to change it, change the *.proto and run `make proto`. 6 | package proto 7 | -------------------------------------------------------------------------------- /terraform/addrs/count_attr.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | // CountAttr is the address of an attribute of the "count" object in 4 | // the interpolation scope, like "count.index". 5 | type CountAttr struct { 6 | referenceable 7 | Name string 8 | } 9 | 10 | func (ca CountAttr) String() string { 11 | return "count." + ca.Name 12 | } 13 | -------------------------------------------------------------------------------- /terraform/addrs/self.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | // Self is the address of the special object "self" that behaves as an alias 4 | // for a containing object currently in scope. 5 | const Self selfT = 0 6 | 7 | type selfT int 8 | 9 | func (s selfT) referenceableSigil() { 10 | } 11 | 12 | func (s selfT) String() string { 13 | return "self" 14 | } 15 | -------------------------------------------------------------------------------- /terraform/README.md: -------------------------------------------------------------------------------- 1 | # Forked Terraform packages 2 | 3 | This directory contains a subset of code from Terraform's internal packages. However, the implementation is not exactly the same, it is just a fork, and simplifications and changes have been made according to our project. 4 | 5 | This package provides functionality for static analysis of Terraform Language. 6 | -------------------------------------------------------------------------------- /terraform/addrs/terraform_attr.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | // TerraformAttr is the address of an attribute of the "terraform" object in 4 | // the interpolation scope, like "terraform.workspace". 5 | type TerraformAttr struct { 6 | referenceable 7 | Name string 8 | } 9 | 10 | func (ta TerraformAttr) String() string { 11 | return "terraform." + ta.Name 12 | } 13 | -------------------------------------------------------------------------------- /terraform/addrs/for_each_attr.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | // ForEachAttr is the address of an attribute referencing the current "for_each" object in 4 | // the interpolation scope, addressed using the "each" keyword, ex. "each.key" and "each.value" 5 | type ForEachAttr struct { 6 | referenceable 7 | Name string 8 | } 9 | 10 | func (f ForEachAttr) String() string { 11 | return "each." + f.Name 12 | } 13 | -------------------------------------------------------------------------------- /tflint/config.go: -------------------------------------------------------------------------------- 1 | package tflint 2 | 3 | // Config is a TFLint configuration applied to the plugin. 4 | type Config struct { 5 | Rules map[string]*RuleConfig 6 | DisabledByDefault bool 7 | Only []string 8 | Fix bool 9 | } 10 | 11 | // RuleConfig is a TFLint's rule configuration. 12 | type RuleConfig struct { 13 | Name string 14 | Enabled bool 15 | } 16 | -------------------------------------------------------------------------------- /logger/doc.go: -------------------------------------------------------------------------------- 1 | // Package logger provides a global logger interface for logging from plugins. 2 | // 3 | // This package is a wrapper for hclog, and it initializes the global logger on import. 4 | // You can freely write logs from anywhere via the public API according to the log level. 5 | // The log by hclog is interpreted as a structured log by go-plugin, and the log level 6 | // can be handled correctly. 7 | package logger 8 | -------------------------------------------------------------------------------- /plugin/host2plugin/doc.go: -------------------------------------------------------------------------------- 1 | // Package host2plugin exposes a gRPC client for use on a host (TFLint). 2 | // 3 | // The implementation details are hidden in internal/host2plugin and 4 | // the exposed ones are minimal. They are not intended to be used by plugins. 5 | // For that reason, this package is subject to breaking changes without notice, 6 | // and the changes do not follow the SDK versioning policy. 7 | package host2plugin 8 | -------------------------------------------------------------------------------- /plugin/plugin2host/doc.go: -------------------------------------------------------------------------------- 1 | // Package plugin2host exposes a gRPC server for use on a host (TFLint). 2 | // 3 | // The implementation details are hidden in internal/plugin2host and 4 | // the exposed ones are minimal. They are not intended to be used by plugins. 5 | // For that reason, this package is subject to breaking changes without notice, 6 | // and the changes do not follow the SDK versioning policy. 7 | package plugin2host 8 | -------------------------------------------------------------------------------- /terraform/lang/doc.go: -------------------------------------------------------------------------------- 1 | // Package lang is a fork of Terraform's internal/lang package. 2 | // 3 | // This package provides helpers that interprets the Terraform Language's semantics 4 | // in more detail than the HCL Language. 5 | // 6 | // For example, ReferencesInExpr returns a set of references, such as input variables 7 | // and resources, rather than just a set of hcl.Traversal, filtering out invalid references. 8 | package lang 9 | -------------------------------------------------------------------------------- /helper/doc.go: -------------------------------------------------------------------------------- 1 | // Package helper contains implementations for plugin testing. 2 | // 3 | // You can test the implemented rules using the mock Runner that is not 4 | // an gRPC client. It is similar to TFLint's Runner, but is implemented 5 | // from scratch to avoid Terraform dependencies. 6 | // 7 | // Some implementations of the mock Runner have been simplified. As a result, 8 | // note that some features may not behave exactly as they should. 9 | package helper 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prepare: 2 | curl -sSfL https://raw.githubusercontent.com/aquaproj/aqua-installer/v3.0.1/aqua-installer | bash 3 | @echo '' 4 | @echo 'Add $${AQUA_ROOT_DIR}/bin to the environment variable PATH.' 5 | @echo 'export PATH="$${AQUA_ROOT_DIR:-$${XDG_DATA_HOME:-$$HOME/.local/share}/aquaproj-aqua}/bin:$$PATH"' 6 | 7 | proto: 8 | cd plugin/internal/proto; \ 9 | protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative tflint.proto 10 | -------------------------------------------------------------------------------- /tflint/doc.go: -------------------------------------------------------------------------------- 1 | // Package tflint contains implementations and interfaces for 2 | // plugin developers. 3 | // 4 | // Each rule can use the gRPC client that satisfies the Runner 5 | // interface as an argument. Through this client, developers 6 | // can get attributes, blocks, and resources to be analyzed 7 | // and send issues to TFLint. 8 | // 9 | // All rules must be implemented to satisfy the Rule interface 10 | // and a plugin must serve the RuleSet that bundles the rules. 11 | package tflint 12 | -------------------------------------------------------------------------------- /aqua.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # aqua - Declarative CLI Version Manager 3 | # https://aquaproj.github.io/ 4 | # checksum: 5 | # enabled: true 6 | # require_checksum: true 7 | # supported_envs: 8 | # - all 9 | registries: 10 | - type: standard 11 | ref: v4.292.0 # renovate: depName=aquaproj/aqua-registry 12 | packages: 13 | - name: protocolbuffers/protobuf/protoc@v29.2 14 | - name: protocolbuffers/protobuf-go/protoc-gen-go@v1.36.1 15 | - name: grpc/grpc-go/protoc-gen-go-grpc@cmd/protoc-gen-go-grpc/v1.5.1 16 | -------------------------------------------------------------------------------- /helper/issue.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/terraform-linters/tflint-plugin-sdk/tflint" 6 | ) 7 | 8 | // Issue is a stub that has the same structure as the actually used issue object. 9 | // This is only used for testing, as the mock Runner doesn't depend on the actual Issue structure. 10 | type Issue struct { 11 | Rule tflint.Rule 12 | Message string 13 | Range hcl.Range 14 | } 15 | 16 | // Issues is a list of Issue. 17 | type Issues []*Issue 18 | -------------------------------------------------------------------------------- /plugin/internal/toproto/doc.go: -------------------------------------------------------------------------------- 1 | // Package toproto contains an implementation to encode a Go structure 2 | // into a structure generated from *.proto. This package is not intended 3 | // to be used directly from plugins. 4 | // 5 | // Many primitives can be handled as-is, but some interfaces and errors 6 | // require special encoding. The `hcl.Expression` encodes into the range 7 | // and the text representation as bytes. Error is encoded into gRPC error 8 | // details to represent wrapped errors. 9 | package toproto 10 | -------------------------------------------------------------------------------- /plugin/host2plugin/host2plugin.go: -------------------------------------------------------------------------------- 1 | package host2plugin 2 | 3 | import "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/host2plugin" 4 | 5 | // Client is a host-side implementation. Host can send requests through the client to plugin's gRPC server. 6 | type Client = host2plugin.GRPCClient 7 | 8 | // ClientOpts is an option for initializing a Client. 9 | type ClientOpts = host2plugin.ClientOpts 10 | 11 | // NewClient returns a new gRPC client for host-to-plugin communication. 12 | var NewClient = host2plugin.NewClient 13 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | actions: write 8 | 9 | jobs: 10 | dependabot: 11 | runs-on: ubuntu-latest 12 | if: github.event.pull_request.user.login == 'dependabot[bot]' 13 | steps: 14 | - name: Enable auto-merge for Dependabot PRs 15 | run: gh pr merge --auto --squash "$PR_URL" 16 | env: 17 | PR_URL: ${{github.event.pull_request.html_url}} 18 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 19 | -------------------------------------------------------------------------------- /terraform/addrs/referenceable.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | // Referenceable is an interface implemented by all address types that can 4 | // appear as references in configuration language expressions. 5 | type Referenceable interface { 6 | referenceableSigil() 7 | 8 | // String produces a string representation of the address that could be 9 | // parsed as a HCL traversal and passed to ParseRef to produce an identical 10 | // result. 11 | String() string 12 | } 13 | 14 | type referenceable struct { 15 | } 16 | 17 | func (r referenceable) referenceableSigil() { 18 | } 19 | -------------------------------------------------------------------------------- /terraform/addrs/module.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | import "strings" 4 | 5 | // Module represents the structure of the module tree. 6 | type Module []string 7 | 8 | // IsRoot returns true if the receiver is the address of the root module, 9 | // or false otherwise. 10 | func (m Module) IsRoot() bool { 11 | return len(m) == 0 12 | } 13 | 14 | // String returns a string representation. 15 | func (m Module) String() string { 16 | if len(m) == 0 { 17 | return "" 18 | } 19 | var steps []string 20 | for _, s := range m { 21 | steps = append(steps, "module", s) 22 | } 23 | return strings.Join(steps, ".") 24 | } 25 | -------------------------------------------------------------------------------- /tflint/issue.go: -------------------------------------------------------------------------------- 1 | package tflint 2 | 3 | // Severity indicates the severity of the issue. 4 | type Severity int32 5 | 6 | const ( 7 | // ERROR is possible errors 8 | ERROR Severity = iota 9 | // WARNING doesn't cause problem immediately, but not good 10 | WARNING 11 | // NOTICE is not important, it's mentioned 12 | NOTICE 13 | ) 14 | 15 | // String returns the string representation of the severity. 16 | func (s Severity) String() string { 17 | switch s { 18 | case ERROR: 19 | return "Error" 20 | case WARNING: 21 | return "Warning" 22 | case NOTICE: 23 | return "Notice" 24 | } 25 | 26 | return "Unknown" 27 | } 28 | -------------------------------------------------------------------------------- /plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/host2plugin" 5 | 6 | // Import this package to initialize the global logger 7 | _ "github.com/terraform-linters/tflint-plugin-sdk/logger" 8 | ) 9 | 10 | // ServeOpts is an option for serving a plugin. 11 | // Each plugin can pass a RuleSet that represents its own functionality. 12 | type ServeOpts = host2plugin.ServeOpts 13 | 14 | // Serve is a wrapper of plugin.Serve. This is entrypoint of all plugins. 15 | var Serve = host2plugin.Serve 16 | 17 | // SDKVersion is the SDK version. 18 | const SDKVersion = host2plugin.SDKVersion 19 | -------------------------------------------------------------------------------- /plugin/internal/fromproto/doc.go: -------------------------------------------------------------------------------- 1 | // Package fromproto contains an implementation to decode a structure 2 | // generated from *.proto into a real Go structure. This package is not 3 | // intended to be used directly from plugins. 4 | // 5 | // Many primitives can be handled as-is, but some interfaces and errors 6 | // require special decoding. The `hcl.Expression` restores the interface 7 | // by reparsed based on the bytes and their range. The `tflint.Rule` 8 | // restores the interface by filling the value in a pseudo-structure that 9 | // satisfies the interface. Error makes use of gRPC error details to recover 10 | // the wrapped error. Rewrap the error based on the error code obtained 11 | // from details. 12 | package fromproto 13 | -------------------------------------------------------------------------------- /plugin/internal/plugin2host/doc.go: -------------------------------------------------------------------------------- 1 | // Package plugin2host contains a gRPC server (host) and client (plugin). 2 | // 3 | // Communication from the plugin to the host is the second one that occurs. 4 | // To understand what happens first, see the host2plugin package first. 5 | // The gRPC client used by the plugin is implicitly initialized by the host2plugin 6 | // package and hidden in the tflint.Runner interface. Normally, plugin developers 7 | // do not need to be aware of the details of this client. 8 | // 9 | // The host starts a gRPC server as goroutine to respond from the plugin side 10 | // when calling Check function in host2plugin. Please note that the gRPC server 11 | // and client startup in plugin2host is not due to go-plugin. 12 | package plugin2host 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | - cron: "0 0 * * *" 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | test: 18 | name: ${{ matrix.os }} 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | os: [ubuntu-latest, windows-latest] 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 26 | - name: Set up Go 27 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 28 | with: 29 | go-version-file: 'go.mod' 30 | - name: Run test 31 | run: go test ./... 32 | -------------------------------------------------------------------------------- /plugin/internal/host2plugin/doc.go: -------------------------------------------------------------------------------- 1 | // Package host2plugin contains a gRPC server (plugin) and client (host). 2 | // 3 | // In the plugin system, this communication is the first thing that happens, 4 | // and a plugin must use this package to provide a gRPC server. 5 | // However, the detailed implementation is hidden in the tflint.RuleSet interface, 6 | // and plugin developers usually don't need to be aware of gRPC server behavior. 7 | // 8 | // When the host initializes a gRPC client, go-plugin starts a gRPC server 9 | // on the plugin side as another process. This package acts as a wrapper for go-plugin. 10 | // Separately, the Check function initializes a new gRPC client for plugin-to-host 11 | // communication. See the plugin2host package for details. 12 | package host2plugin 13 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | checks: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 20 | - name: Set up Go 21 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 22 | with: 23 | go-version-file: 'go.mod' 24 | - name: Run linters 25 | run: | 26 | export PATH=$PATH:$(go env GOPATH)/bin 27 | go install golang.org/x/lint/golint@latest 28 | golint --set_exit_status $(go list ./...) 29 | go vet ./... 30 | -------------------------------------------------------------------------------- /tflint/expandmode_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ExpandMode"; DO NOT EDIT. 2 | 3 | package tflint 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ExpandModeExpand-0] 12 | _ = x[ExpandModeNone-1] 13 | } 14 | 15 | const _ExpandMode_name = "ExpandModeExpandExpandModeNone" 16 | 17 | var _ExpandMode_index = [...]uint8{0, 16, 30} 18 | 19 | func (i ExpandMode) String() string { 20 | if i < 0 || i >= ExpandMode(len(_ExpandMode_index)-1) { 21 | return "ExpandMode(" + strconv.FormatInt(int64(i), 10) + ")" 22 | } 23 | return _ExpandMode_name[_ExpandMode_index[i]:_ExpandMode_index[i+1]] 24 | } 25 | -------------------------------------------------------------------------------- /hclext/schemamode_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=SchemaMode"; DO NOT EDIT. 2 | 3 | package hclext 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[SchemaDefaultMode-0] 12 | _ = x[SchemaJustAttributesMode-1] 13 | } 14 | 15 | const _SchemaMode_name = "SchemaDefaultModeSchemaJustAttributesMode" 16 | 17 | var _SchemaMode_index = [...]uint8{0, 17, 41} 18 | 19 | func (i SchemaMode) String() string { 20 | if i < 0 || i >= SchemaMode(len(_SchemaMode_index)-1) { 21 | return "SchemaMode(" + strconv.FormatInt(int64(i), 10) + ")" 22 | } 23 | return _SchemaMode_name[_SchemaMode_index[i]:_SchemaMode_index[i+1]] 24 | } 25 | -------------------------------------------------------------------------------- /tflint/walker.go: -------------------------------------------------------------------------------- 1 | package tflint 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | ) 6 | 7 | // ExprWalker is an interface used with WalkExpressions. 8 | type ExprWalker interface { 9 | Enter(expr hcl.Expression) hcl.Diagnostics 10 | Exit(expr hcl.Expression) hcl.Diagnostics 11 | } 12 | 13 | // ExprWalkFunc is the callback signature for WalkExpressions. 14 | // This satisfies the ExprWalker interface. 15 | type ExprWalkFunc func(expr hcl.Expression) hcl.Diagnostics 16 | 17 | // Enter is a function of ExprWalker that invokes itself on the passed expression. 18 | func (f ExprWalkFunc) Enter(expr hcl.Expression) hcl.Diagnostics { 19 | return f(expr) 20 | } 21 | 22 | // Exit is one of ExprWalker's functions, noop here 23 | func (f ExprWalkFunc) Exit(expr hcl.Expression) hcl.Diagnostics { 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /tflint/modulectxtype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ModuleCtxType"; DO NOT EDIT. 2 | 3 | package tflint 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[SelfModuleCtxType-0] 12 | _ = x[RootModuleCtxType-1] 13 | } 14 | 15 | const _ModuleCtxType_name = "SelfModuleCtxTypeRootModuleCtxType" 16 | 17 | var _ModuleCtxType_index = [...]uint8{0, 17, 34} 18 | 19 | func (i ModuleCtxType) String() string { 20 | if i < 0 || i >= ModuleCtxType(len(_ModuleCtxType_index)-1) { 21 | return "ModuleCtxType(" + strconv.FormatInt(int64(i), 10) + ")" 22 | } 23 | return _ModuleCtxType_name[_ModuleCtxType_index[i]:_ModuleCtxType_index[i+1]] 24 | } 25 | -------------------------------------------------------------------------------- /plugin/internal/interceptor/logging.go: -------------------------------------------------------------------------------- 1 | package interceptor 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/terraform-linters/tflint-plugin-sdk/logger" 7 | "google.golang.org/grpc" 8 | ) 9 | 10 | // RequestLogging is an interceptor for gRPC request logging. 11 | // It outouts all request logs as "trace" level, and if an error occurs, 12 | // it outputs the response as "error" level. 13 | func RequestLogging(direction string) grpc.UnaryServerInterceptor { 14 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 15 | logger.Trace("gRPC request", "direction", direction, "method", info.FullMethod, "req", req) 16 | ret, err := handler(ctx, req) 17 | if err != nil { 18 | logger.Error("failed to gRPC request", "direction", direction, "method", info.FullMethod, "err", err) 19 | } 20 | return ret, err 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /terraform/addrs/resourcemode_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ResourceMode"; DO NOT EDIT. 2 | 3 | package addrs 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[InvalidResourceMode-0] 12 | _ = x[ManagedResourceMode-77] 13 | _ = x[DataResourceMode-68] 14 | } 15 | 16 | const ( 17 | _ResourceMode_name_0 = "InvalidResourceMode" 18 | _ResourceMode_name_1 = "DataResourceMode" 19 | _ResourceMode_name_2 = "ManagedResourceMode" 20 | ) 21 | 22 | func (i ResourceMode) String() string { 23 | switch { 24 | case i == 0: 25 | return _ResourceMode_name_0 26 | case i == 68: 27 | return _ResourceMode_name_1 28 | case i == 77: 29 | return _ResourceMode_name_2 30 | default: 31 | return "ResourceMode(" + strconv.FormatInt(int64(i), 10) + ")" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /hclext/doc.go: -------------------------------------------------------------------------------- 1 | // Package hclext is an extension of package hcl for TFLint. 2 | // 3 | // The goal of this package is to work with nested hcl.BodyContent. 4 | // In the various functions provided by the package hcl, hcl.Block 5 | // nests hcl.Body as body. However, since hcl.Body is an interface, 6 | // the nested body cannot be sent over a wire protocol. 7 | // 8 | // In this package, redefine hcl.Block as hclext.Block nests BodyContent, 9 | // not Body, which is an interface. Some functions and related structures 10 | // have been redefined to make hclext.Block behave like the package hcl. 11 | // 12 | // For example, Content/PartialContent takes hclext.BodySchema instead of 13 | // hcl.BodySchema and returns hclext.BodyContent. In hclext.BodySchema, 14 | // you can declare the structure of the nested body as the block schema. 15 | // This allows you to send the schema and its results of configurations 16 | // that contain nested bodies via gRPC. 17 | package hclext 18 | -------------------------------------------------------------------------------- /tflint/rule.go: -------------------------------------------------------------------------------- 1 | package tflint 2 | 3 | // DefaultRule implements optional fields in the rule interface. 4 | // You can create a rule by embedding this rule. 5 | type DefaultRule struct{} 6 | 7 | // Link allows you to add a reference link to the rule. 8 | // The default is empty. 9 | func (r *DefaultRule) Link() string { 10 | return "" 11 | } 12 | 13 | // Metadata allows you to set any metadata to the rule. 14 | // This value is never referenced by the SDK and can be used for your custom ruleset. 15 | func (r *DefaultRule) Metadata() interface{} { 16 | return nil 17 | } 18 | 19 | func (r *DefaultRule) mustEmbedDefaultRule() {} 20 | 21 | var _ Rule = &embedDefaultRule{} 22 | 23 | type embedDefaultRule struct { 24 | DefaultRule 25 | } 26 | 27 | func (r *embedDefaultRule) Name() string { return "" } 28 | func (r *embedDefaultRule) Enabled() bool { return true } 29 | func (r *embedDefaultRule) Severity() Severity { return ERROR } 30 | func (r *embedDefaultRule) Check(runner Runner) error { return nil } 31 | -------------------------------------------------------------------------------- /terraform/addrs/module_call.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ModuleCall is the address of a call from the current module to a child 8 | // module. 9 | type ModuleCall struct { 10 | referenceable 11 | Name string 12 | } 13 | 14 | func (c ModuleCall) String() string { 15 | return "module." + c.Name 16 | } 17 | 18 | // ModuleCallInstance is the address of one instance of a module created from 19 | // a module call, which might create multiple instances using "count" or 20 | // "for_each" arguments. 21 | type ModuleCallInstance struct { 22 | referenceable 23 | Call ModuleCall 24 | Key InstanceKey 25 | } 26 | 27 | func (c ModuleCallInstance) String() string { 28 | if c.Key == NoKey { 29 | return c.Call.String() 30 | } 31 | return fmt.Sprintf("module.%s%s", c.Call.Name, c.Key) 32 | } 33 | 34 | // ModuleCallInstanceOutput is the address of a particular named output produced by 35 | // an instance of a module call. 36 | type ModuleCallInstanceOutput struct { 37 | referenceable 38 | Call ModuleCallInstance 39 | Name string 40 | } 41 | 42 | func (co ModuleCallInstanceOutput) String() string { 43 | return fmt.Sprintf("%s.%s", co.Call.String(), co.Name) 44 | } 45 | -------------------------------------------------------------------------------- /tflint/errors.go: -------------------------------------------------------------------------------- 1 | package tflint 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // List of errors returned by TFLint. 8 | var ( 9 | // ErrUnknownValue is an error that occurs when decoding an unknown value to a Go value. 10 | ErrUnknownValue = errors.New("unknown value found") 11 | // ErrNullValue is an error that occurs when decoding null to a Go value. 12 | ErrNullValue = errors.New("null value found") 13 | // ErrUnevaluable is an error that occurs when decoding an unevaluable value to a Go value. 14 | // 15 | // Deprecated: This error is no longer returned since TFLint v0.41. 16 | ErrUnevaluable = errors.New("") 17 | // ErrSensitive is an error that occurs when decoding a sensitive value to a Go value. 18 | ErrSensitive = errors.New("sensitive value found") 19 | // ErrEphemeral is an error that occurs when decoding an ephemeral value to a Go value. 20 | ErrEphemeral = errors.New("ephemeral value found") 21 | ) 22 | 23 | var ( 24 | // ErrFixNotSupported is an error to return if autofix is not supported. 25 | // This can prevent the issue from being marked as fixable by returning it 26 | // in FixFunc when autofix cannot be implemented, such as with JSON syntax. 27 | ErrFixNotSupported = errors.New("autofix is not supported") 28 | ) 29 | -------------------------------------------------------------------------------- /terraform/lang/marks/marks.go: -------------------------------------------------------------------------------- 1 | package marks 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | ) 6 | 7 | // valueMarks allow creating strictly typed values for use as cty.Value marks. 8 | // Each distinct mark value must be a constant in this package whose value 9 | // is a valueMark whose underlying string matches the name of the variable. 10 | type valueMark string 11 | 12 | func (m valueMark) GoString() string { 13 | return "marks." + string(m) 14 | } 15 | 16 | // Has returns true if and only if the cty.Value has the given mark. 17 | func Has(val cty.Value, mark valueMark) bool { 18 | return val.HasMark(mark) 19 | } 20 | 21 | // Contains returns true if the cty.Value or any any value within it contains 22 | // the given mark. 23 | func Contains(val cty.Value, mark valueMark) bool { 24 | ret := false 25 | cty.Walk(val, func(_ cty.Path, v cty.Value) (bool, error) { 26 | if v.HasMark(mark) { 27 | ret = true 28 | return false, nil 29 | } 30 | return true, nil 31 | }) 32 | return ret 33 | } 34 | 35 | // Sensitive indicates that this value is marked as sensitive in the context of 36 | // Terraform. 37 | const Sensitive = valueMark("Sensitive") 38 | 39 | // Ephemeral indicates that this value is marked as ephemeral in the context of 40 | // Terraform. 41 | const Ephemeral = valueMark("Ephemeral") 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/terraform-linters/tflint-plugin-sdk 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/go-test/deep v1.1.1 9 | github.com/google/go-cmp v0.7.0 10 | github.com/hashicorp/go-hclog v1.6.3 11 | github.com/hashicorp/go-plugin v1.7.0 12 | github.com/hashicorp/go-version v1.7.0 13 | github.com/hashicorp/hcl/v2 v2.24.0 14 | github.com/zclconf/go-cty v1.17.0 15 | golang.org/x/tools v0.40.0 16 | google.golang.org/grpc v1.77.0 17 | google.golang.org/protobuf v1.36.11 18 | ) 19 | 20 | require ( 21 | github.com/agext/levenshtein v1.2.1 // indirect 22 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 23 | github.com/fatih/color v1.13.0 // indirect 24 | github.com/golang/protobuf v1.5.4 // indirect 25 | github.com/hashicorp/yamux v0.1.2 // indirect 26 | github.com/mattn/go-colorable v0.1.12 // indirect 27 | github.com/mattn/go-isatty v0.0.17 // indirect 28 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 29 | github.com/oklog/run v1.1.0 // indirect 30 | github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect 31 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 32 | golang.org/x/mod v0.31.0 // indirect 33 | golang.org/x/net v0.48.0 // indirect 34 | golang.org/x/sync v0.19.0 // indirect 35 | golang.org/x/sys v0.39.0 // indirect 36 | golang.org/x/text v0.32.0 // indirect 37 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /hclext/expression.go: -------------------------------------------------------------------------------- 1 | package hclext 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/zclconf/go-cty/cty" 6 | ) 7 | 8 | // BoundExpr represents an expression whose a value is bound. 9 | // This is a wrapper for any expression, typically satisfying 10 | // an interface to behave like the wrapped expression. 11 | // 12 | // The difference is that when resolving a value with `Value()`, 13 | // instead of resolving the variables with EvalContext, 14 | // the bound value is returned directly. 15 | type BoundExpr struct { 16 | Val cty.Value 17 | 18 | original hcl.Expression 19 | } 20 | 21 | var _ hcl.Expression = (*BoundExpr)(nil) 22 | 23 | // BindValue binds the passed value to an expression. 24 | // This returns the bound expression. 25 | func BindValue(val cty.Value, expr hcl.Expression) hcl.Expression { 26 | return &BoundExpr{original: expr, Val: val} 27 | } 28 | 29 | // Value returns the bound value. 30 | func (e BoundExpr) Value(*hcl.EvalContext) (cty.Value, hcl.Diagnostics) { 31 | return e.Val, nil 32 | } 33 | 34 | // Variables delegates to the wrapped expression. 35 | func (e BoundExpr) Variables() []hcl.Traversal { 36 | return e.original.Variables() 37 | } 38 | 39 | // Range delegates to the wrapped expression. 40 | func (e BoundExpr) Range() hcl.Range { 41 | return e.original.Range() 42 | } 43 | 44 | // StartRange delegates to the wrapped expression. 45 | func (e BoundExpr) StartRange() hcl.Range { 46 | return e.original.StartRange() 47 | } 48 | 49 | // UnwrapExpression returns the original expression. 50 | // This satisfies the hcl.unwrapExpression interface. 51 | func (e BoundExpr) UnwrapExpression() hcl.Expression { 52 | return e.original 53 | } 54 | -------------------------------------------------------------------------------- /hclext/parse.go: -------------------------------------------------------------------------------- 1 | package hclext 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/hashicorp/hcl/v2/hclsyntax" 9 | "github.com/hashicorp/hcl/v2/json" 10 | ) 11 | 12 | // ParseExpression is a wrapper that calls ParseExpression of hclsyntax and json based on the file extension. 13 | // This function specializes in parsing intermediate expressions in the file, 14 | // so it takes into account the hack on trailing newlines in heredoc. 15 | func ParseExpression(src []byte, filename string, start hcl.Pos) (hcl.Expression, hcl.Diagnostics) { 16 | // Handle HCL files: .tf (Terraform HCL) and .hcl (HCL config like .tflint.hcl) 17 | if strings.HasSuffix(filename, ".tf") || strings.HasSuffix(filename, ".hcl") { 18 | // HACK: Always add a newline to avoid heredoc parse errors. 19 | // @see https://github.com/hashicorp/hcl/issues/441 20 | src = []byte(string(src) + "\n") 21 | return hclsyntax.ParseExpression(src, filename, start) 22 | } 23 | 24 | // Handle JSON files: 25 | // We accept any .json file (including .tf.json), not just specific ones like .tflint.json. 26 | // The calling functions are responsible for validating that the file should be processed. 27 | // If the content is not valid HCL-compatible JSON, the JSON parser will return appropriate diagnostics. 28 | if strings.HasSuffix(filename, ".json") { 29 | return json.ParseExpressionWithStartPos(src, filename, start) 30 | } 31 | 32 | return nil, hcl.Diagnostics{ 33 | { 34 | Severity: hcl.DiagError, 35 | Summary: "Unexpected file extension", 36 | Detail: fmt.Sprintf("The file name `%s` is a file with an unexpected extension. Valid extensions are `.tf`, `.tf.json`, `.hcl`, and `.json`.", filename), 37 | }, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /plugin/internal/host2plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package host2plugin 2 | 3 | import ( 4 | "context" 5 | 6 | plugin "github.com/hashicorp/go-plugin" 7 | "github.com/hashicorp/go-version" 8 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/proto" 9 | "github.com/terraform-linters/tflint-plugin-sdk/tflint" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | // SDKVersion is the SDK version. 14 | const SDKVersion = "0.23.1" 15 | 16 | // minTFLintVersionConstraint presents the minimum version of TFLint that this SDK supports. 17 | var minTFLintVersionConstraint = version.MustConstraints(version.NewConstraint(">= 0.46")) 18 | 19 | // handShakeConfig is used for UX. ProcotolVersion will be updated by incompatible changes. 20 | var handshakeConfig = plugin.HandshakeConfig{ 21 | ProtocolVersion: 11, 22 | MagicCookieKey: "TFLINT_RULESET_PLUGIN", 23 | MagicCookieValue: "5adSn1bX8nrDfgBqiAqqEkC6OE1h3iD8SqbMc5UUONx8x3xCF0KlPDsBRNDjoYDP", 24 | } 25 | 26 | // RuleSetPlugin is a wrapper to satisfy the interface of go-plugin. 27 | type RuleSetPlugin struct { 28 | plugin.NetRPCUnsupportedPlugin 29 | 30 | impl tflint.RuleSet 31 | } 32 | 33 | var _ plugin.GRPCPlugin = &RuleSetPlugin{} 34 | 35 | // GRPCServer returns an gRPC server acting as a plugin. 36 | func (p *RuleSetPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { 37 | proto.RegisterRuleSetServer(s, &GRPCServer{ 38 | impl: p.impl, 39 | broker: broker, 40 | }) 41 | return nil 42 | } 43 | 44 | // GRPCClient returns an RPC client for the host. 45 | func (*RuleSetPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { 46 | return &GRPCClient{ 47 | client: proto.NewRuleSetClient(c), 48 | broker: broker, 49 | }, nil 50 | } 51 | -------------------------------------------------------------------------------- /tflint/option.go: -------------------------------------------------------------------------------- 1 | package tflint 2 | 3 | import "github.com/zclconf/go-cty/cty" 4 | 5 | // ModuleCtxType represents target module. 6 | // 7 | //go:generate stringer -type=ModuleCtxType 8 | type ModuleCtxType int32 9 | 10 | const ( 11 | // SelfModuleCtxType targets the current module. The default is this behavior. 12 | SelfModuleCtxType ModuleCtxType = iota 13 | // RootModuleCtxType targets the root module. This is useful when you want to refer to a provider config. 14 | RootModuleCtxType 15 | ) 16 | 17 | // ExpandMode represents whether the block retrieved by GetModuleContent is expanded by the meta-arguments. 18 | // 19 | //go:generate stringer -type=ExpandMode 20 | type ExpandMode int32 21 | 22 | const ( 23 | // ExpandModeExpand is the mode for expanding blocks based on the meta-arguments. The default is this behavior. 24 | ExpandModeExpand ExpandMode = iota 25 | // ExpandModeNone is the mode that does not expand blocks. 26 | ExpandModeNone 27 | ) 28 | 29 | // GetModuleContentOption is an option that controls the behavior when getting a module content. 30 | type GetModuleContentOption struct { 31 | // Specify the module to be acquired. 32 | ModuleCtx ModuleCtxType 33 | // Whether resources and modules are expanded by the count/for_each meta-arguments. 34 | ExpandMode ExpandMode 35 | // Hint is info for optimizing a query. This is an advanced option and it is not intended to be used directly from plugins. 36 | Hint GetModuleContentHint 37 | } 38 | 39 | // GetModuleContentHint is info for optimizing a query. This is an advanced option and it is not intended to be used directly from plugins. 40 | type GetModuleContentHint struct { 41 | ResourceType string 42 | } 43 | 44 | // EvaluateExprOption is an option that controls the behavior when evaluating an expression. 45 | type EvaluateExprOption struct { 46 | // Specify what type of value is expected. 47 | WantType *cty.Type 48 | // Set the scope of the module to evaluate. 49 | ModuleCtx ModuleCtxType 50 | } 51 | -------------------------------------------------------------------------------- /terraform/addrs/resource.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Resource is an address for a resource block within configuration, which 8 | // contains potentially-multiple resource instances if that configuration 9 | // block uses "count" or "for_each". 10 | type Resource struct { 11 | referenceable 12 | Mode ResourceMode 13 | Type string 14 | Name string 15 | } 16 | 17 | func (r Resource) String() string { 18 | switch r.Mode { 19 | case ManagedResourceMode: 20 | return fmt.Sprintf("%s.%s", r.Type, r.Name) 21 | case DataResourceMode: 22 | return fmt.Sprintf("data.%s.%s", r.Type, r.Name) 23 | default: 24 | // Should never happen, but we'll return a string here rather than 25 | // crashing just in case it does. 26 | return fmt.Sprintf(".%s.%s", r.Type, r.Name) 27 | } 28 | } 29 | 30 | // ResourceInstance is an address for a specific instance of a resource. 31 | // When a resource is defined in configuration with "count" or "for_each" it 32 | // produces zero or more instances, which can be addressed using this type. 33 | type ResourceInstance struct { 34 | referenceable 35 | Resource Resource 36 | Key InstanceKey 37 | } 38 | 39 | func (r ResourceInstance) String() string { 40 | if r.Key == NoKey { 41 | return r.Resource.String() 42 | } 43 | return r.Resource.String() + r.Key.String() 44 | } 45 | 46 | // ResourceMode defines which lifecycle applies to a given resource. Each 47 | // resource lifecycle has a slightly different address format. 48 | type ResourceMode rune 49 | 50 | //go:generate go run golang.org/x/tools/cmd/stringer -type ResourceMode 51 | 52 | const ( 53 | // InvalidResourceMode is the zero value of ResourceMode and is not 54 | // a valid resource mode. 55 | InvalidResourceMode ResourceMode = 0 56 | 57 | // ManagedResourceMode indicates a managed resource, as defined by 58 | // "resource" blocks in configuration. 59 | ManagedResourceMode ResourceMode = 'M' 60 | 61 | // DataResourceMode indicates a data resource, as defined by 62 | // "data" blocks in configuration. 63 | DataResourceMode ResourceMode = 'D' 64 | ) 65 | -------------------------------------------------------------------------------- /hclext/decode_example_test.go: -------------------------------------------------------------------------------- 1 | package hclext 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | "github.com/hashicorp/hcl/v2/hclsyntax" 8 | ) 9 | 10 | func ExampleDecodeBody() { 11 | src := ` 12 | noodle "foo" "bar" { 13 | type = "rice" 14 | 15 | bread "baz" { 16 | type = "focaccia" 17 | baked = true 18 | } 19 | bread "quz" { 20 | type = "rye" 21 | } 22 | }` 23 | file, diags := hclsyntax.ParseConfig([]byte(src), "test.tf", hcl.InitialPos) 24 | if diags.HasErrors() { 25 | panic(diags) 26 | } 27 | 28 | type Bread struct { 29 | // The `*,label` tag matches "bread" block labels. 30 | // The count of tags should be matched to count of block labels. 31 | Name string `hclext:"name,label"` 32 | // The `type` tag matches a "type" attribute inside of "bread" block. 33 | Type string `hclext:"type"` 34 | // The `baked,optional` tag matches a "baked" attribute, but it is optional. 35 | Baked bool `hclext:"baked,optional"` 36 | } 37 | type Noodle struct { 38 | Name string `hclext:"name,label"` 39 | SubName string `hclext:"subname,label"` 40 | Type string `hclext:"type"` 41 | // The `bread,block` tag matches "bread" blocks. 42 | // Multiple blocks are allowed because the field type is slice. 43 | Breads []Bread `hclext:"bread,block"` 44 | } 45 | type Config struct { 46 | // Only 1 block must be needed because the field type is not slice, not a pointer. 47 | Noodle Noodle `hclext:"noodle,block"` 48 | } 49 | 50 | target := &Config{} 51 | 52 | schema := ImpliedBodySchema(target) 53 | body, diags := Content(file.Body, schema) 54 | if diags.HasErrors() { 55 | panic(diags) 56 | } 57 | 58 | diags = DecodeBody(body, nil, target) 59 | if diags.HasErrors() { 60 | panic(diags) 61 | } 62 | 63 | fmt.Printf("- noodle: name=%s, subname=%s type=%s\n", target.Noodle.Name, target.Noodle.SubName, target.Noodle.Type) 64 | for i, bread := range target.Noodle.Breads { 65 | fmt.Printf(" - bread[%d]: name=%s, type=%s baked=%t\n", i, bread.Name, bread.Type, bread.Baked) 66 | } 67 | // Output: 68 | // - noodle: name=foo, subname=bar type=rice 69 | // - bread[0]: name=baz, type=focaccia baked=true 70 | // - bread[1]: name=quz, type=rye baked=false 71 | } 72 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/hashicorp/go-hclog" 7 | ) 8 | 9 | // internalLogger is intended to be called via the public methods of the package. 10 | // So the output line will be the caller of this package. 11 | var internalLogger hclog.Logger 12 | 13 | // logger is inteded to be called directly. 14 | // It is mainly assumed to be used by go-plugin. 15 | var logger hclog.Logger 16 | 17 | // Use the init process to set the global logger. 18 | // It is expected to be initialized when the plugin starts 19 | // and you need to import the package in the proper order. 20 | func init() { 21 | level := os.Getenv("TFLINT_LOG") 22 | if level == "" { 23 | // Do not emit logs by default 24 | level = "off" 25 | } 26 | 27 | internalLogger = hclog.New(&hclog.LoggerOptions{ 28 | Level: hclog.LevelFromString(level), 29 | Output: os.Stderr, 30 | TimeFormat: "15:04:05", 31 | IncludeLocation: true, 32 | AdditionalLocationOffset: 1, 33 | }) 34 | logger = hclog.New(&hclog.LoggerOptions{ 35 | Level: hclog.LevelFromString(level), 36 | Output: os.Stderr, 37 | TimeFormat: "15:04:05", 38 | IncludeLocation: true, 39 | }) 40 | } 41 | 42 | // Logger returns hcl.Logger 43 | func Logger() hclog.Logger { 44 | return logger 45 | } 46 | 47 | // Trace emits a message at the TRACE level 48 | func Trace(msg string, args ...interface{}) { 49 | if internalLogger == nil { 50 | return 51 | } 52 | internalLogger.Trace(msg, args...) 53 | } 54 | 55 | // Debug emits a message at the DEBUG level 56 | func Debug(msg string, args ...interface{}) { 57 | if internalLogger == nil { 58 | return 59 | } 60 | internalLogger.Debug(msg, args...) 61 | } 62 | 63 | // Info emits a message at the INFO level 64 | func Info(msg string, args ...interface{}) { 65 | if internalLogger == nil { 66 | return 67 | } 68 | internalLogger.Info(msg, args...) 69 | } 70 | 71 | // Warn emits a message at the WARN level 72 | func Warn(msg string, args ...interface{}) { 73 | if internalLogger == nil { 74 | return 75 | } 76 | internalLogger.Warn(msg, args...) 77 | } 78 | 79 | // Error emits a message at the ERROR level 80 | func Error(msg string, args ...interface{}) { 81 | if internalLogger == nil { 82 | return 83 | } 84 | internalLogger.Error(msg, args...) 85 | } 86 | -------------------------------------------------------------------------------- /terraform/lang/references.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/terraform-linters/tflint-plugin-sdk/terraform/addrs" 6 | ) 7 | 8 | // References finds all of the references in the given set of traversals, 9 | // returning diagnostics if any of the traversals cannot be interpreted as a 10 | // reference. 11 | // 12 | // This function does not do any de-duplication of references, since references 13 | // have source location information embedded in them and so any invalid 14 | // references that are duplicated should have errors reported for each 15 | // occurence. 16 | // 17 | // If the returned diagnostics contains errors then the result may be 18 | // incomplete or invalid. Otherwise, the returned slice has one reference per 19 | // given traversal, though it is not guaranteed that the references will 20 | // appear in the same order as the given traversals. 21 | func References(traversals []hcl.Traversal) ([]*addrs.Reference, hcl.Diagnostics) { 22 | if len(traversals) == 0 { 23 | return nil, nil 24 | } 25 | 26 | var diags hcl.Diagnostics 27 | refs := make([]*addrs.Reference, 0, len(traversals)) 28 | 29 | for _, traversal := range traversals { 30 | ref, refDiags := addrs.ParseRef(traversal) 31 | diags = diags.Extend(refDiags) 32 | if ref == nil { 33 | continue 34 | } 35 | refs = append(refs, ref) 36 | } 37 | 38 | return refs, diags 39 | } 40 | 41 | // ReferencesInExpr is a helper wrapper around References that first searches 42 | // the given expression for traversals, before converting those traversals 43 | // to references. 44 | // 45 | // This function is almost identical to the Terraform internal API of the same name, 46 | // except that it does not return diagnostics if it contains an invalid reference. 47 | // This is because expressions with invalid traversals as references, such as 48 | // `ignore_changes`, may be parsed. Developers should take advantage of the possible 49 | // incomplete results returned by this function. 50 | // 51 | // Low-level APIs such as addrs.ParseRef are recommended if the expression is 52 | // guaranteed not to contain invalid traversals, and analysis should stop in that case. 53 | func ReferencesInExpr(expr hcl.Expression) []*addrs.Reference { 54 | if expr == nil { 55 | return nil 56 | } 57 | traversals := expr.Variables() 58 | refs, _ := References(traversals) 59 | return refs 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TFLint plugin SDK 2 | [![Build Status](https://github.com/terraform-linters/tflint-plugin-sdk/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/terraform-linters/tflint-plugin-sdk/actions) 3 | [![GitHub release](https://img.shields.io/github/release/terraform-linters/tflint-plugin-sdk.svg)](https://github.com/terraform-linters/tflint-plugin-sdk/releases/latest) 4 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/terraform-linters/tflint-plugin-sdk) 5 | [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-blue.svg)](LICENSE) 6 | 7 | [TFLint](https://github.com/terraform-linters/tflint) plugin SDK for building custom rules. 8 | 9 | NOTE: This plugin system is experimental. This means that API compatibility is frequently broken. 10 | 11 | ## Requirements 12 | 13 | - TFLint v0.46+ 14 | - Go v1.24+ 15 | 16 | ## Usage 17 | 18 | Please refer to [tflint-ruleset-template](https://github.com/terraform-linters/tflint-ruleset-template) for an example plugin implementation using this SDK. 19 | 20 | For more details on the API, see https://pkg.go.dev/github.com/terraform-linters/tflint-plugin-sdk. 21 | 22 | ## Developing 23 | 24 | The proto compiler is required when updating `.proto` files. The `protoc` and `protoc-gen-go` can be installed using [aqua](https://github.com/aquaproj/aqua). 25 | 26 | ```console 27 | $ make prepare 28 | curl -sSfL https://raw.githubusercontent.com/aquaproj/aqua-installer/v2.1.1/aqua-installer | bash 29 | ===> Installing aqua v2.2.3 for bootstraping... 30 | 31 | ... 32 | 33 | aqua version 2.3.7 (c07105b10ab825e7f309d2eb83278a0422a2b24f) 34 | 35 | Add ${AQUA_ROOT_DIR}/bin to the environment variable PATH. 36 | export PATH="${AQUA_ROOT_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/aquaproj-aqua}/bin:$PATH" 37 | $ export PATH="${AQUA_ROOT_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/aquaproj-aqua}/bin:$PATH" 38 | $ aqua install 39 | $ make proto 40 | ``` 41 | 42 | ## Architecture 43 | 44 | This plugin system uses [go-plugin](https://github.com/hashicorp/go-plugin). TFLint launch plugins as sub-processes and communicates with plugins over gRPC. A plugin acts as a server, while TFLint acts as a client that sends inspection requests to the plugin. 45 | 46 | On the other hand, the plugin sends various requests to a server (TFLint) to get detailed runtime contexts (e.g. variables and expressions). This means that TFLint and plugins can act as both a server and a client. 47 | 48 | This SDK provides client, server and Protocol Buffers for these bi-directional communications. See [Architecture](https://github.com/terraform-linters/tflint/blob/master/docs/developer-guide/architecture.md) for more details. 49 | -------------------------------------------------------------------------------- /hclext/parse_test.go: -------------------------------------------------------------------------------- 1 | package hclext 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | ) 9 | 10 | func TestParseExpression(t *testing.T) { 11 | tests := []struct { 12 | Name string 13 | Source string 14 | Filename string 15 | Want string 16 | DiagCount int 17 | }{ 18 | { 19 | Name: "HCL (*.tf)", 20 | Source: `"foo"`, 21 | Filename: "test.tf", 22 | Want: `cty.StringVal("foo")`, 23 | DiagCount: 0, 24 | }, 25 | { 26 | Name: "HCL (*.hcl)", 27 | Source: `"bar"`, 28 | Filename: "test.hcl", 29 | Want: `cty.StringVal("bar")`, 30 | DiagCount: 0, 31 | }, 32 | { 33 | Name: "JSON (*.json)", 34 | Source: `"baz"`, 35 | Filename: "test.json", 36 | Want: `cty.StringVal("baz")`, 37 | DiagCount: 0, 38 | }, 39 | { 40 | Name: "JSON (.tflint.json)", 41 | Source: `{"config": {"force": true}}`, 42 | Filename: ".tflint.json", 43 | Want: `cty.ObjectVal(map[string]cty.Value{"config":cty.ObjectVal(map[string]cty.Value{"force":cty.True})})`, 44 | DiagCount: 0, 45 | }, 46 | { 47 | Name: "HCL heredoc with trailing newline", 48 | Source: `< got[j].SourceRange.String() 111 | }) 112 | 113 | for _, problem := range deep.Equal(got, test.Want) { 114 | t.Error(problem) 115 | } 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /terraform/addrs/instance_key.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | "github.com/zclconf/go-cty/cty/gocty" 8 | ) 9 | 10 | // InstanceKey represents the key of an instance within an object that 11 | // contains multiple instances due to using "count" or "for_each" arguments 12 | // in configuration. 13 | // 14 | // IntKey and StringKey are the two implementations of this type. No other 15 | // implementations are allowed. The single instance of an object that _isn't_ 16 | // using "count" or "for_each" is represented by NoKey, which is a nil 17 | // InstanceKey. 18 | type InstanceKey interface { 19 | instanceKeySigil() 20 | String() string 21 | 22 | // Value returns the cty.Value of the appropriate type for the InstanceKey 23 | // value. 24 | Value() cty.Value 25 | } 26 | 27 | // ParseInstanceKey returns the instance key corresponding to the given value, 28 | // which must be known and non-null. 29 | // 30 | // If an unknown or null value is provided then this function will panic. This 31 | // function is intended to deal with the values that would naturally be found 32 | // in a hcl.TraverseIndex, which (when parsed from source, at least) can never 33 | // contain unknown or null values. 34 | func ParseInstanceKey(key cty.Value) (InstanceKey, error) { 35 | switch key.Type() { 36 | case cty.String: 37 | return StringKey(key.AsString()), nil 38 | case cty.Number: 39 | var idx int 40 | err := gocty.FromCtyValue(key, &idx) 41 | return IntKey(idx), err 42 | default: 43 | return NoKey, fmt.Errorf("either a string or an integer is required") 44 | } 45 | } 46 | 47 | // NoKey represents the absense of an InstanceKey, for the single instance 48 | // of a configuration object that does not use "count" or "for_each" at all. 49 | var NoKey InstanceKey 50 | 51 | // IntKey is the InstanceKey representation representing integer indices, as 52 | // used when the "count" argument is specified or if for_each is used with 53 | // a sequence type. 54 | type IntKey int 55 | 56 | func (k IntKey) instanceKeySigil() { 57 | } 58 | 59 | func (k IntKey) String() string { 60 | return fmt.Sprintf("[%d]", int(k)) 61 | } 62 | 63 | // Value returns a cty.Value of type Number 64 | func (k IntKey) Value() cty.Value { 65 | return cty.NumberIntVal(int64(k)) 66 | } 67 | 68 | // StringKey is the InstanceKey representation representing string indices, as 69 | // used when the "for_each" argument is specified with a map or object type. 70 | type StringKey string 71 | 72 | func (k StringKey) instanceKeySigil() { 73 | } 74 | 75 | func (k StringKey) String() string { 76 | // FIXME: This isn't _quite_ right because Go's quoted string syntax is 77 | // slightly different than HCL's, but we'll accept it for now. 78 | return fmt.Sprintf("[%q]", string(k)) 79 | } 80 | 81 | // Value returns a cty.Value of type String 82 | func (k StringKey) Value() cty.Value { 83 | return cty.StringVal(string(k)) 84 | } 85 | 86 | // InstanceKeyType represents the different types of instance key that are 87 | // supported. Usually it is sufficient to simply type-assert an InstanceKey 88 | // value to either IntKey or StringKey, but this type and its values can be 89 | // used to represent the types themselves, rather than specific values 90 | // of those types. 91 | type InstanceKeyType rune 92 | 93 | const ( 94 | // NoKeyType is a type of NoKey instance key 95 | NoKeyType InstanceKeyType = 0 96 | // IntKeyType is a type of IntKey instance key 97 | IntKeyType InstanceKeyType = 'I' 98 | // StringKeyType is a type of StringKey instance key 99 | StringKeyType InstanceKeyType = 'S' 100 | ) 101 | -------------------------------------------------------------------------------- /tflint/ruleset_test.go: -------------------------------------------------------------------------------- 1 | package tflint 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | type testRule struct { 10 | DefaultRule 11 | name string 12 | } 13 | 14 | func (r *testRule) Name() string { return r.name } 15 | func (r *testRule) Enabled() bool { return true } 16 | func (r *testRule) Severity() Severity { return ERROR } 17 | func (r *testRule) Check(Runner) error { return nil } 18 | 19 | type testRule1 struct { 20 | testRule 21 | } 22 | 23 | type testRule2 struct { 24 | testRule 25 | } 26 | 27 | type testRule3 struct { 28 | testRule 29 | } 30 | 31 | func TestApplyGlobalConfig(t *testing.T) { 32 | tests := []struct { 33 | name string 34 | config *Config 35 | want []string 36 | }{ 37 | { 38 | name: "default", 39 | config: &Config{}, 40 | want: []string{"test_rule1", "test_rule2", "test_rule3"}, 41 | }, 42 | { 43 | name: "disabled by default", 44 | config: &Config{DisabledByDefault: true}, 45 | want: []string{}, 46 | }, 47 | { 48 | name: "rule config", 49 | config: &Config{ 50 | Rules: map[string]*RuleConfig{ 51 | "test_rule1": { 52 | Name: "test_rule1", 53 | Enabled: false, 54 | }, 55 | }, 56 | }, 57 | want: []string{"test_rule2", "test_rule3"}, 58 | }, 59 | { 60 | name: "only", 61 | config: &Config{Only: []string{"test_rule1"}}, 62 | want: []string{"test_rule1"}, 63 | }, 64 | { 65 | name: "disabled by default + rule config", 66 | config: &Config{ 67 | Rules: map[string]*RuleConfig{ 68 | "test_rule2": { 69 | Name: "test_rule2", 70 | Enabled: true, 71 | }, 72 | }, 73 | DisabledByDefault: true, 74 | }, 75 | want: []string{"test_rule2"}, 76 | }, 77 | { 78 | name: "only + rule config", 79 | config: &Config{ 80 | Rules: map[string]*RuleConfig{ 81 | "test_rule1": { 82 | Name: "test_rule1", 83 | Enabled: false, 84 | }, 85 | }, 86 | Only: []string{"test_rule1", "test_rule2"}, 87 | }, 88 | want: []string{"test_rule1", "test_rule2"}, 89 | }, 90 | { 91 | name: "disabled by default + only", 92 | config: &Config{ 93 | DisabledByDefault: true, 94 | Only: []string{"test_rule1", "test_rule2"}, 95 | }, 96 | want: []string{"test_rule1", "test_rule2"}, 97 | }, 98 | { 99 | name: "disabled by default + only + rule config", 100 | config: &Config{ 101 | Rules: map[string]*RuleConfig{ 102 | "test_rule2": { 103 | Name: "test_rule2", 104 | Enabled: true, 105 | }, 106 | "test_rule3": { 107 | Name: "test_rule3", 108 | Enabled: false, 109 | }, 110 | }, 111 | DisabledByDefault: true, 112 | Only: []string{"test_rule1", "test_rule3"}, 113 | }, 114 | want: []string{"test_rule1", "test_rule3"}, 115 | }, 116 | } 117 | 118 | for _, test := range tests { 119 | t.Run(test.name, func(t *testing.T) { 120 | ruleset := &BuiltinRuleSet{ 121 | Rules: []Rule{ 122 | &testRule1{testRule: testRule{name: "test_rule1"}}, 123 | &testRule2{testRule: testRule{name: "test_rule2"}}, 124 | &testRule3{testRule: testRule{name: "test_rule3"}}, 125 | }, 126 | } 127 | 128 | if err := ruleset.ApplyGlobalConfig(test.config); err != nil { 129 | t.Fatal(err) 130 | } 131 | 132 | got := make([]string, len(ruleset.EnabledRules)) 133 | for i, r := range ruleset.EnabledRules { 134 | got[i] = r.Name() 135 | } 136 | 137 | if diff := cmp.Diff(got, test.want); diff != "" { 138 | t.Error(diff) 139 | } 140 | }) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tflint/ruleset.go: -------------------------------------------------------------------------------- 1 | package tflint 2 | 3 | import ( 4 | "github.com/terraform-linters/tflint-plugin-sdk/hclext" 5 | "github.com/terraform-linters/tflint-plugin-sdk/logger" 6 | ) 7 | 8 | var _ RuleSet = &BuiltinRuleSet{} 9 | 10 | // BuiltinRuleSet is the basis of the ruleset. Plugins can serve this ruleset directly. 11 | // You can serve a custom ruleset by embedding this ruleset if you need special extensions. 12 | type BuiltinRuleSet struct { 13 | Name string 14 | Version string 15 | Constraint string 16 | Rules []Rule 17 | 18 | EnabledRules []Rule 19 | } 20 | 21 | // RuleSetName is the name of the ruleset. 22 | // Generally, this is synonymous with the name of the plugin. 23 | func (r *BuiltinRuleSet) RuleSetName() string { 24 | return r.Name 25 | } 26 | 27 | // RuleSetVersion is the version of the plugin. 28 | func (r *BuiltinRuleSet) RuleSetVersion() string { 29 | return r.Version 30 | } 31 | 32 | // RuleNames is a list of rule names provided by the plugin. 33 | func (r *BuiltinRuleSet) RuleNames() []string { 34 | names := make([]string, len(r.Rules)) 35 | for idx, rule := range r.Rules { 36 | names[idx] = rule.Name() 37 | } 38 | return names 39 | } 40 | 41 | // VersionConstraint declares the version of TFLint the plugin will work with. 42 | // Default is no constraint. 43 | func (r *BuiltinRuleSet) VersionConstraint() string { 44 | return r.Constraint 45 | } 46 | 47 | // ApplyGlobalConfig applies the common config to the ruleset. 48 | // This is not supposed to be overridden from custom rulesets. 49 | // Override the ApplyConfig if you want to apply the plugin's own configuration. 50 | // 51 | // The priority of rule configs is as follows: 52 | // 53 | // 1. --only option 54 | // 2. Rule config declared in each "rule" block 55 | // 3. The `disabled_by_default` declared in global "config" block 56 | func (r *BuiltinRuleSet) ApplyGlobalConfig(config *Config) error { 57 | r.EnabledRules = []Rule{} 58 | only := map[string]bool{} 59 | 60 | if len(config.Only) > 0 { 61 | logger.Debug("Only mode is enabled. Ignoring default plugin rules") 62 | for _, rule := range config.Only { 63 | only[rule] = true 64 | } 65 | } else if config.DisabledByDefault { 66 | logger.Debug("Default plugin rules are disabled by default") 67 | } 68 | 69 | for _, rule := range r.Rules { 70 | enabled := rule.Enabled() 71 | if len(only) > 0 { 72 | enabled = only[rule.Name()] 73 | } else if cfg := config.Rules[rule.Name()]; cfg != nil { 74 | enabled = cfg.Enabled 75 | } else if config.DisabledByDefault { 76 | enabled = false 77 | } 78 | 79 | if enabled { 80 | r.EnabledRules = append(r.EnabledRules, rule) 81 | } 82 | } 83 | return nil 84 | } 85 | 86 | // ConfigSchema returns the ruleset plugin config schema. 87 | // This schema should be a schema inside of "plugin" block. 88 | // Custom rulesets can override this method to return the plugin's own config schema. 89 | func (r *BuiltinRuleSet) ConfigSchema() *hclext.BodySchema { 90 | return nil 91 | } 92 | 93 | // ApplyConfig applies the configuration to the ruleset. 94 | // Custom rulesets can override this method to reflect the plugin's own configuration. 95 | func (r *BuiltinRuleSet) ApplyConfig(content *hclext.BodyContent) error { 96 | return nil 97 | } 98 | 99 | // NewRunner returns a new runner based on the original runner. 100 | // Custom rulesets can override this method to inject a custom runner. 101 | func (r *BuiltinRuleSet) NewRunner(runner Runner) (Runner, error) { 102 | return runner, nil 103 | } 104 | 105 | // BuiltinImpl returns the receiver itself as BuiltinRuleSet. 106 | // This is not supposed to be overridden from custom rulesets. 107 | func (r *BuiltinRuleSet) BuiltinImpl() *BuiltinRuleSet { 108 | return r 109 | } 110 | 111 | func (r *BuiltinRuleSet) mustEmbedBuiltinRuleSet() {} 112 | -------------------------------------------------------------------------------- /internal/token_scanner.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | "github.com/hashicorp/hcl/v2/hclsyntax" 8 | ) 9 | 10 | // tokenScanner is a token-based scanner for HCL. 11 | // The scanner scans tokens one by one and returns its position and token details. 12 | type tokenScanner struct { 13 | tokens hclsyntax.Tokens 14 | pos hcl.Pos 15 | index int 16 | filename string 17 | } 18 | 19 | func newTokenScanner(source []byte, filename string) (*tokenScanner, hcl.Diagnostics) { 20 | tokens, diags := hclsyntax.LexConfig(source, filename, hcl.InitialPos) 21 | if diags.HasErrors() { 22 | return nil, diags 23 | } 24 | return &tokenScanner{ 25 | tokens: tokens, 26 | pos: hcl.InitialPos, 27 | index: 0, 28 | filename: filename, 29 | }, nil 30 | } 31 | 32 | type tokenPos int 33 | 34 | const ( 35 | tokenStart tokenPos = iota 36 | tokenEnd 37 | ) 38 | 39 | // seek moves the currnet position to the given position. 40 | // The destination token is determined by the given match condtion. 41 | // 42 | // match tokenStart: 43 | // 44 | // | <- pos 45 | // foo=1 46 | // |-| token is "=" 47 | // 48 | // match tokenEnd: 49 | // 50 | // | <- pos 51 | // foo=1 52 | // |-| token is "foo" 53 | func (s *tokenScanner) seek(to hcl.Pos, match tokenPos) error { 54 | switch { 55 | case s.tokenPos(match).Byte == to.Byte: 56 | return nil 57 | case to.Byte < s.tokenPos(match).Byte: 58 | for s.scanBackward() { 59 | if to.Byte == s.tokenPos(match).Byte { 60 | s.pos = to 61 | return nil 62 | } 63 | } 64 | case s.tokenPos(match).Byte < to.Byte: 65 | for s.scan() { 66 | if s.tokenPos(match).Byte == to.Byte { 67 | s.pos = to 68 | return nil 69 | } 70 | } 71 | default: 72 | panic("unreachable") 73 | } 74 | 75 | return fmt.Errorf("no token found at %s:%d,%d", s.filename, to.Line, to.Column) 76 | } 77 | 78 | func (s *tokenScanner) seekByIndex(idx int, pos tokenPos) error { 79 | if idx < 0 || len(s.tokens) <= idx { 80 | return fmt.Errorf("index out of range: %d", idx) 81 | } 82 | s.index = idx 83 | s.pos = s.tokenPos(pos) 84 | return nil 85 | } 86 | 87 | // seekTokenStart moves the current position to the start of the current token. 88 | func (s *tokenScanner) seekTokenStart() { 89 | s.pos = s.token().Range.Start 90 | } 91 | 92 | func (s *tokenScanner) seekTokenEnd() { 93 | s.pos = s.token().Range.End 94 | } 95 | 96 | // scan moves the current position to the next token. 97 | // position is always set to the end of the token. 98 | func (s *tokenScanner) scan() bool { 99 | i := s.index + 1 100 | if i >= len(s.tokens) { 101 | s.seekTokenEnd() 102 | return false 103 | } 104 | s.index = i 105 | s.seekTokenEnd() 106 | return true 107 | } 108 | 109 | func (s *tokenScanner) scanIf(tokenType hclsyntax.TokenType) bool { 110 | i := s.index + 1 111 | if i >= len(s.tokens) { 112 | return false 113 | } 114 | if s.tokens[i].Type != tokenType { 115 | return false 116 | } 117 | s.scan() 118 | return true 119 | } 120 | 121 | // scanBackward moves the current position to the previous token. 122 | // position is always set to the start of the token. 123 | func (s *tokenScanner) scanBackward() bool { 124 | i := s.index - 1 125 | if i < 0 { 126 | s.seekTokenStart() 127 | return false 128 | } 129 | s.index = i 130 | s.seekTokenStart() 131 | return true 132 | } 133 | 134 | func (s *tokenScanner) scanBackwardIf(tokenType hclsyntax.TokenType) bool { 135 | i := s.index - 1 136 | if i < 0 { 137 | return false 138 | } 139 | if s.tokens[i].Type != tokenType { 140 | return false 141 | } 142 | s.scanBackward() 143 | return true 144 | } 145 | 146 | func (s *tokenScanner) token() hclsyntax.Token { 147 | return s.tokens[s.index] 148 | } 149 | 150 | func (s *tokenScanner) tokenPos(pos tokenPos) hcl.Pos { 151 | switch pos { 152 | case tokenStart: 153 | return s.token().Range.Start 154 | case tokenEnd: 155 | return s.token().Range.End 156 | default: 157 | panic("unreachable") 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /helper/testing.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/google/go-cmp/cmp/cmpopts" 11 | "github.com/hashicorp/hcl/v2" 12 | "github.com/hashicorp/hcl/v2/gohcl" 13 | "github.com/hashicorp/hcl/v2/hclparse" 14 | "github.com/terraform-linters/tflint-plugin-sdk/tflint" 15 | ) 16 | 17 | // TestRunner returns a mock Runner for testing. 18 | // You can pass the map of file names and their contents in the second argument. 19 | func TestRunner(t *testing.T, files map[string]string) *Runner { 20 | t.Helper() 21 | 22 | runner := newLocalRunner(map[string]*hcl.File{}, Issues{}) 23 | parser := hclparse.NewParser() 24 | 25 | for name, src := range files { 26 | var file *hcl.File 27 | var diags hcl.Diagnostics 28 | if strings.HasSuffix(name, ".json") { 29 | file, diags = parser.ParseJSON([]byte(src), name) 30 | } else { 31 | file, diags = parser.ParseHCL([]byte(src), name) 32 | } 33 | if diags.HasErrors() { 34 | t.Fatal(diags) 35 | } 36 | 37 | if name == ".tflint.hcl" { 38 | var config Config 39 | if diags := gohcl.DecodeBody(file.Body, nil, &config); diags.HasErrors() { 40 | t.Fatal(diags) 41 | } 42 | runner.config = config 43 | } else { 44 | runner.addLocalFile(name, file) 45 | } 46 | } 47 | 48 | if err := runner.initFromFiles(); err != nil { 49 | panic(fmt.Sprintf("Failed to initialize runner: %s", err)) 50 | } 51 | return runner 52 | } 53 | 54 | // AssertIssues is an assertion helper for comparing issues. 55 | func AssertIssues(t *testing.T, want Issues, got Issues) { 56 | t.Helper() 57 | 58 | opts := []cmp.Option{ 59 | // Byte field will be ignored because it's not important in tests such as positions 60 | cmpopts.IgnoreFields(hcl.Pos{}, "Byte"), 61 | // Issues will be sorted and output in the end, so ignore the order. 62 | ignoreIssuesOrder(), 63 | ruleComparer(), 64 | } 65 | if diff := cmp.Diff(want, got, opts...); diff != "" { 66 | t.Fatalf("Expected issues are not matched:\n %s\n", diff) 67 | } 68 | } 69 | 70 | // AssertIssuesWithoutRange is an assertion helper for comparing issues except for range. 71 | func AssertIssuesWithoutRange(t *testing.T, want Issues, got Issues) { 72 | t.Helper() 73 | 74 | opts := []cmp.Option{ 75 | cmpopts.IgnoreFields(Issue{}, "Range"), 76 | ignoreIssuesOrder(), 77 | ruleComparer(), 78 | } 79 | if diff := cmp.Diff(want, got, opts...); diff != "" { 80 | t.Fatalf("Expected issues are not matched:\n %s\n", diff) 81 | } 82 | } 83 | 84 | // AssertChanges is an assertion helper for comparing autofix changes. 85 | func AssertChanges(t *testing.T, want map[string]string, got map[string][]byte) { 86 | t.Helper() 87 | 88 | sources := make(map[string]string) 89 | for name, src := range got { 90 | sources[name] = string(src) 91 | } 92 | if diff := cmp.Diff(want, sources); diff != "" { 93 | t.Fatalf("Expected changes are not matched:\n %s\n", diff) 94 | } 95 | } 96 | 97 | // ruleComparer returns a Comparer func that checks that two rule interfaces 98 | // have the same underlying type. It does not compare struct fields. 99 | func ruleComparer() cmp.Option { 100 | return cmp.Comparer(func(x, y tflint.Rule) bool { 101 | return reflect.TypeOf(x) == reflect.TypeOf(y) 102 | }) 103 | } 104 | 105 | func ignoreIssuesOrder() cmp.Option { 106 | return cmpopts.SortSlices(func(i, j *Issue) bool { 107 | if i.Range.Filename != j.Range.Filename { 108 | return i.Range.Filename < j.Range.Filename 109 | } 110 | if i.Range.Start.Line != j.Range.Start.Line { 111 | return i.Range.Start.Line < j.Range.Start.Line 112 | } 113 | if i.Range.Start.Column != j.Range.Start.Column { 114 | return i.Range.Start.Column < j.Range.Start.Column 115 | } 116 | if i.Range.End.Line != j.Range.End.Line { 117 | return i.Range.End.Line > j.Range.End.Line 118 | } 119 | if i.Range.End.Column != j.Range.End.Column { 120 | return i.Range.End.Column > j.Range.End.Column 121 | } 122 | return i.Message < j.Message 123 | }) 124 | } 125 | -------------------------------------------------------------------------------- /hclext/schema_test.go: -------------------------------------------------------------------------------- 1 | package hclext 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func TestImpliedBodySchema(t *testing.T) { 10 | tests := []struct { 11 | Name string 12 | Val interface{} 13 | Want *BodySchema 14 | }{ 15 | { 16 | Name: "empty struct", 17 | Val: struct{}{}, 18 | Want: &BodySchema{}, 19 | }, 20 | { 21 | Name: "struct without tags", 22 | Val: struct { 23 | Ignored bool 24 | }{}, 25 | Want: &BodySchema{}, 26 | }, 27 | { 28 | Name: "attribute tags", 29 | Val: struct { 30 | Attr1 bool `hclext:"attr1"` 31 | Attr2 bool `hclext:"attr2"` 32 | }{}, 33 | Want: &BodySchema{ 34 | Attributes: []AttributeSchema{ 35 | { 36 | Name: "attr1", 37 | Required: true, 38 | }, 39 | { 40 | Name: "attr2", 41 | Required: true, 42 | }, 43 | }, 44 | }, 45 | }, 46 | { 47 | Name: "pointer attribute tags", 48 | Val: struct { 49 | Attr *bool `hclext:"attr,attr"` 50 | }{}, 51 | Want: &BodySchema{ 52 | Attributes: []AttributeSchema{ 53 | { 54 | Name: "attr", 55 | Required: false, 56 | }, 57 | }, 58 | }, 59 | }, 60 | { 61 | Name: "optional attribute tags", 62 | Val: struct { 63 | Attr bool `hclext:"attr,optional"` 64 | }{}, 65 | Want: &BodySchema{ 66 | Attributes: []AttributeSchema{ 67 | { 68 | Name: "attr", 69 | Required: false, 70 | }, 71 | }, 72 | }, 73 | }, 74 | { 75 | Name: "block tags", 76 | Val: struct { 77 | Thing struct{} `hclext:"thing,block"` 78 | }{}, 79 | Want: &BodySchema{ 80 | Blocks: []BlockSchema{ 81 | { 82 | Type: "thing", 83 | Body: &BodySchema{}, 84 | }, 85 | }, 86 | }, 87 | }, 88 | { 89 | Name: "block tags with labels", 90 | Val: struct { 91 | Thing struct { 92 | Type string `hclext:"type,label"` 93 | Name string `hclext:"name,label"` 94 | } `hclext:"thing,block"` 95 | }{}, 96 | Want: &BodySchema{ 97 | Blocks: []BlockSchema{ 98 | { 99 | Type: "thing", 100 | LabelNames: []string{"type", "name"}, 101 | Body: &BodySchema{}, 102 | }, 103 | }, 104 | }, 105 | }, 106 | { 107 | Name: "multiple block tags with labels", 108 | Val: struct { 109 | Thing []struct { 110 | Type string `hclext:"type,label"` 111 | Name string `hclext:"name,label"` 112 | } `hclext:"thing,block"` 113 | }{}, 114 | Want: &BodySchema{ 115 | Blocks: []BlockSchema{ 116 | { 117 | Type: "thing", 118 | LabelNames: []string{"type", "name"}, 119 | Body: &BodySchema{}, 120 | }, 121 | }, 122 | }, 123 | }, 124 | { 125 | Name: "pointer block tags with labels", 126 | Val: struct { 127 | Thing *struct { 128 | Type string `hclext:"type,label"` 129 | Name string `hclext:"name,label"` 130 | } `hclext:"thing,block"` 131 | }{}, 132 | Want: &BodySchema{ 133 | Blocks: []BlockSchema{ 134 | { 135 | Type: "thing", 136 | LabelNames: []string{"type", "name"}, 137 | Body: &BodySchema{}, 138 | }, 139 | }, 140 | }, 141 | }, 142 | { 143 | Name: "nested block tags with labels", 144 | Val: struct { 145 | Thing struct { 146 | Name string `hclext:"name,label"` 147 | Something string `hclext:"something"` 148 | } `hclext:"thing,block"` 149 | }{}, 150 | Want: &BodySchema{ 151 | Blocks: []BlockSchema{ 152 | { 153 | Type: "thing", 154 | LabelNames: []string{"name"}, 155 | Body: &BodySchema{ 156 | Attributes: []AttributeSchema{ 157 | { 158 | Name: "something", 159 | Required: true, 160 | }, 161 | }, 162 | }, 163 | }, 164 | }, 165 | }, 166 | }, 167 | { 168 | Name: "attribute/block tags with labels", 169 | Val: struct { 170 | Doodad string `hclext:"doodad"` 171 | Thing struct { 172 | Name string `hclext:"name,label"` 173 | } `hclext:"thing,block"` 174 | }{}, 175 | Want: &BodySchema{ 176 | Attributes: []AttributeSchema{ 177 | { 178 | Name: "doodad", 179 | Required: true, 180 | }, 181 | }, 182 | Blocks: []BlockSchema{ 183 | { 184 | Type: "thing", 185 | LabelNames: []string{"name"}, 186 | Body: &BodySchema{}, 187 | }, 188 | }, 189 | }, 190 | }, 191 | } 192 | 193 | for _, test := range tests { 194 | t.Run(test.Name, func(t *testing.T) { 195 | got := ImpliedBodySchema(test.Val) 196 | if diff := cmp.Diff(test.Want, got); diff != "" { 197 | t.Errorf("wrong schema\ndiff: %s", diff) 198 | } 199 | }) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /plugin/internal/host2plugin/client.go: -------------------------------------------------------------------------------- 1 | package host2plugin 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | 7 | "github.com/hashicorp/go-plugin" 8 | "github.com/hashicorp/go-version" 9 | "github.com/terraform-linters/tflint-plugin-sdk/hclext" 10 | "github.com/terraform-linters/tflint-plugin-sdk/logger" 11 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/fromproto" 12 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/interceptor" 13 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/plugin2host" 14 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/proto" 15 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/toproto" 16 | "github.com/terraform-linters/tflint-plugin-sdk/tflint" 17 | "google.golang.org/grpc" 18 | ) 19 | 20 | // GRPCClient is a host-side implementation. Host can send requests through the client to plugin's gRPC server. 21 | type GRPCClient struct { 22 | broker *plugin.GRPCBroker 23 | client proto.RuleSetClient 24 | } 25 | 26 | // ClientOpts is an option for initializing a Client. 27 | type ClientOpts struct { 28 | Cmd *exec.Cmd 29 | } 30 | 31 | // NewClient is a wrapper of plugin.NewClient. 32 | func NewClient(opts *ClientOpts) *plugin.Client { 33 | return plugin.NewClient(&plugin.ClientConfig{ 34 | HandshakeConfig: handshakeConfig, 35 | Plugins: map[string]plugin.Plugin{ 36 | "ruleset": &RuleSetPlugin{}, 37 | }, 38 | Cmd: opts.Cmd, 39 | AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, 40 | Logger: logger.Logger(), 41 | }) 42 | } 43 | 44 | // RuleSetName returns the name of a plugin. 45 | func (c *GRPCClient) RuleSetName() (string, error) { 46 | resp, err := c.client.GetName(context.Background(), &proto.GetName_Request{}) 47 | if err != nil { 48 | return "", fromproto.Error(err) 49 | } 50 | return resp.Name, nil 51 | } 52 | 53 | // RuleSetVersion returns the version of a plugin. 54 | func (c *GRPCClient) RuleSetVersion() (string, error) { 55 | resp, err := c.client.GetVersion(context.Background(), &proto.GetVersion_Request{}) 56 | if err != nil { 57 | return "", fromproto.Error(err) 58 | } 59 | return resp.Version, nil 60 | } 61 | 62 | // RuleNames returns the list of rule names provided by a plugin. 63 | func (c *GRPCClient) RuleNames() ([]string, error) { 64 | resp, err := c.client.GetRuleNames(context.Background(), &proto.GetRuleNames_Request{}) 65 | if err != nil { 66 | return []string{}, fromproto.Error(err) 67 | } 68 | return resp.Names, nil 69 | } 70 | 71 | // VersionConstraints returns constraints of TFLint versions. 72 | func (c *GRPCClient) VersionConstraints() (version.Constraints, error) { 73 | resp, err := c.client.GetVersionConstraint(context.Background(), &proto.GetVersionConstraint_Request{}) 74 | if err != nil { 75 | return nil, fromproto.Error(err) 76 | } 77 | 78 | if resp.Constraint == "" { 79 | return version.Constraints{}, nil 80 | } 81 | return version.NewConstraint(resp.Constraint) 82 | } 83 | 84 | // SDKVersion returns the SDK version. 85 | func (c *GRPCClient) SDKVersion() (*version.Version, error) { 86 | resp, err := c.client.GetSDKVersion(context.Background(), &proto.GetSDKVersion_Request{}) 87 | if err != nil { 88 | return nil, fromproto.Error(err) 89 | } 90 | return version.NewVersion(resp.Version) 91 | } 92 | 93 | // ConfigSchema fetches the config schema from a plugin. 94 | func (c *GRPCClient) ConfigSchema() (*hclext.BodySchema, error) { 95 | resp, err := c.client.GetConfigSchema(context.Background(), &proto.GetConfigSchema_Request{}) 96 | if err != nil { 97 | return nil, fromproto.Error(err) 98 | } 99 | return fromproto.BodySchema(resp.Schema), nil 100 | } 101 | 102 | // ApplyGlobalConfig applies a common config to a plugin. 103 | func (c *GRPCClient) ApplyGlobalConfig(config *tflint.Config) error { 104 | _, err := c.client.ApplyGlobalConfig(context.Background(), &proto.ApplyGlobalConfig_Request{Config: toproto.Config(config)}) 105 | if err != nil { 106 | return fromproto.Error(err) 107 | } 108 | return nil 109 | } 110 | 111 | // ApplyConfig applies the config to a plugin. 112 | func (c *GRPCClient) ApplyConfig(content *hclext.BodyContent, sources map[string][]byte) error { 113 | _, err := c.client.ApplyConfig(context.Background(), &proto.ApplyConfig_Request{Content: toproto.BodyContent(content, sources)}) 114 | if err != nil { 115 | return fromproto.Error(err) 116 | } 117 | return nil 118 | } 119 | 120 | // Check calls its own plugin implementation with an gRPC client that can send 121 | // requests to the host process. 122 | func (c *GRPCClient) Check(runner plugin2host.Server) error { 123 | brokerID := c.broker.NextId() 124 | logger.Debug("starting host-side gRPC server") 125 | go c.broker.AcceptAndServe(brokerID, func(opts []grpc.ServerOption) *grpc.Server { 126 | opts = append(opts, grpc.UnaryInterceptor(interceptor.RequestLogging("plugin2host"))) 127 | server := grpc.NewServer(opts...) 128 | proto.RegisterRunnerServer(server, &plugin2host.GRPCServer{Impl: runner}) 129 | return server 130 | }) 131 | 132 | _, err := c.client.Check(context.Background(), &proto.Check_Request{Runner: brokerID}) 133 | 134 | if err != nil { 135 | return fromproto.Error(err) 136 | } 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /hclext/schema.go: -------------------------------------------------------------------------------- 1 | package hclext 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | // SchemaMode controls how the body's schema is declared. 11 | // 12 | //go:generate stringer -type=SchemaMode 13 | type SchemaMode int32 14 | 15 | const ( 16 | // SchemaDefaultMode is a mode for explicitly declaring the structure of attributes and blocks. 17 | SchemaDefaultMode SchemaMode = iota 18 | // SchemaJustAttributesMode is the mode to extract body as attributes. 19 | // In this mode you don't need to declare schema for attributes or blocks. 20 | SchemaJustAttributesMode 21 | ) 22 | 23 | // BodySchema represents the desired body. 24 | // This structure is designed to have attributes similar to hcl.BodySchema. 25 | type BodySchema struct { 26 | Mode SchemaMode 27 | Attributes []AttributeSchema 28 | Blocks []BlockSchema 29 | } 30 | 31 | // AttributeSchema represents the desired attribute. 32 | // This structure is designed to have attributes similar to hcl.AttributeSchema. 33 | type AttributeSchema struct { 34 | Name string 35 | Required bool 36 | } 37 | 38 | // BlockSchema represents the desired block header and body schema. 39 | // Unlike hcl.BlockHeaderSchema, this can set nested body schema. 40 | // Instead, hclext.Block can't handle abstract values like hcl.Body, 41 | // so you need to specify all nested schemas at once. 42 | type BlockSchema struct { 43 | Type string 44 | LabelNames []string 45 | 46 | Body *BodySchema 47 | } 48 | 49 | // ImpliedBodySchema is a derivative of gohcl.ImpliedBodySchema that produces hclext.BodySchema instead of hcl.BodySchema. 50 | // Unlike gohcl.ImpliedBodySchema, it produces nested schemas. 51 | // This method differs from gohcl.DecodeBody in several ways: 52 | // 53 | // - Does not support `body` and `remain` tags. 54 | // - Does not support partial schema. 55 | // 56 | // @see https://github.com/hashicorp/hcl/blob/v2.11.1/gohcl/schema.go 57 | func ImpliedBodySchema(val interface{}) *BodySchema { 58 | return impliedBodySchema(reflect.TypeOf(val)) 59 | } 60 | 61 | func impliedBodySchema(ty reflect.Type) *BodySchema { 62 | if ty.Kind() == reflect.Ptr { 63 | ty = ty.Elem() 64 | } 65 | 66 | if ty.Kind() != reflect.Struct { 67 | panic(fmt.Sprintf("given type must be struct, not %s", ty.Name())) 68 | } 69 | 70 | var attrSchemas []AttributeSchema 71 | var blockSchemas []BlockSchema 72 | 73 | tags := getFieldTags(ty) 74 | 75 | attrNames := make([]string, 0, len(tags.Attributes)) 76 | for n := range tags.Attributes { 77 | attrNames = append(attrNames, n) 78 | } 79 | sort.Strings(attrNames) 80 | for _, n := range attrNames { 81 | idx := tags.Attributes[n] 82 | optional := tags.Optional[n] 83 | field := ty.Field(idx) 84 | 85 | var required bool 86 | 87 | switch { 88 | case field.Type.Kind() != reflect.Ptr && !optional: 89 | required = true 90 | default: 91 | required = false 92 | } 93 | 94 | attrSchemas = append(attrSchemas, AttributeSchema{ 95 | Name: n, 96 | Required: required, 97 | }) 98 | } 99 | 100 | blockNames := make([]string, 0, len(tags.Blocks)) 101 | for n := range tags.Blocks { 102 | blockNames = append(blockNames, n) 103 | } 104 | sort.Strings(blockNames) 105 | for _, n := range blockNames { 106 | idx := tags.Blocks[n] 107 | field := ty.Field(idx) 108 | fty := field.Type 109 | if fty.Kind() == reflect.Slice { 110 | fty = fty.Elem() 111 | } 112 | if fty.Kind() == reflect.Ptr { 113 | fty = fty.Elem() 114 | } 115 | if fty.Kind() != reflect.Struct { 116 | panic(fmt.Sprintf( 117 | "schema 'block' tag kind cannot be applied to %s field %s: struct required", field.Type.String(), field.Name, 118 | )) 119 | } 120 | ftags := getFieldTags(fty) 121 | var labelNames []string 122 | if len(ftags.Labels) > 0 { 123 | labelNames = make([]string, len(ftags.Labels)) 124 | for i, l := range ftags.Labels { 125 | labelNames[i] = l.Name 126 | } 127 | } 128 | 129 | blockSchemas = append(blockSchemas, BlockSchema{ 130 | Type: n, 131 | LabelNames: labelNames, 132 | Body: impliedBodySchema(fty), 133 | }) 134 | } 135 | 136 | return &BodySchema{ 137 | Attributes: attrSchemas, 138 | Blocks: blockSchemas, 139 | } 140 | } 141 | 142 | type fieldTags struct { 143 | Attributes map[string]int 144 | Blocks map[string]int 145 | Labels []labelField 146 | Optional map[string]bool 147 | } 148 | 149 | type labelField struct { 150 | FieldIndex int 151 | Name string 152 | } 153 | 154 | func getFieldTags(ty reflect.Type) *fieldTags { 155 | ret := &fieldTags{ 156 | Attributes: map[string]int{}, 157 | Blocks: map[string]int{}, 158 | Optional: map[string]bool{}, 159 | } 160 | 161 | ct := ty.NumField() 162 | for i := 0; i < ct; i++ { 163 | field := ty.Field(i) 164 | tag := field.Tag.Get("hclext") 165 | if tag == "" { 166 | continue 167 | } 168 | 169 | comma := strings.Index(tag, ",") 170 | var name, kind string 171 | if comma != -1 { 172 | name = tag[:comma] 173 | kind = tag[comma+1:] 174 | } else { 175 | name = tag 176 | kind = "attr" 177 | } 178 | 179 | switch kind { 180 | case "attr": 181 | ret.Attributes[name] = i 182 | case "block": 183 | ret.Blocks[name] = i 184 | case "label": 185 | ret.Labels = append(ret.Labels, labelField{ 186 | FieldIndex: i, 187 | Name: name, 188 | }) 189 | case "optional": 190 | ret.Attributes[name] = i 191 | ret.Optional[name] = true 192 | case "remain", "body": 193 | panic(fmt.Sprintf("'%s' tag is permitted in HCL, but not permitted in schema", kind)) 194 | default: 195 | panic(fmt.Sprintf("invalid schema field tag kind %q on %s %q", kind, field.Type.String(), field.Name)) 196 | } 197 | } 198 | 199 | return ret 200 | } 201 | -------------------------------------------------------------------------------- /hclext/decode.go: -------------------------------------------------------------------------------- 1 | package hclext 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/hashicorp/hcl/v2" 9 | "github.com/hashicorp/hcl/v2/gohcl" 10 | ) 11 | 12 | // DecodeBody is a derivative of gohcl.DecodeBody the receives hclext.BodyContent instead of hcl.Body. 13 | // Since hcl.Body is hard to send over a wire protocol, it is needed to support BodyContent. 14 | // This method differs from gohcl.DecodeBody in several ways: 15 | // 16 | // - Does not support decoding to map, cty.Value, hcl.Body, hcl.Expression. 17 | // - Does not support `body` and `remain` tags. 18 | // - Extraneous attributes are always ignored. 19 | // 20 | // @see https://github.com/hashicorp/hcl/blob/v2.11.1/gohcl/decode.go 21 | func DecodeBody(body *BodyContent, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { 22 | rv := reflect.ValueOf(val) 23 | if rv.Kind() != reflect.Ptr { 24 | panic(fmt.Sprintf("target value must be a pointer, not %s", rv.Type().String())) 25 | } 26 | 27 | return decodeBody(body, ctx, rv.Elem()) 28 | } 29 | 30 | func decodeBody(body *BodyContent, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { 31 | if body == nil { 32 | return nil 33 | } 34 | 35 | et := val.Type() 36 | switch et.Kind() { 37 | case reflect.Struct: 38 | return decodeBodyToStruct(body, ctx, val) 39 | default: 40 | panic(fmt.Sprintf("target value must be pointer to struct, not %s", et.String())) 41 | } 42 | } 43 | 44 | func decodeBodyToStruct(body *BodyContent, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { 45 | var diags hcl.Diagnostics 46 | 47 | tags := getFieldTags(val.Type()) 48 | 49 | for name, fieldIdx := range tags.Attributes { 50 | attr, exists := body.Attributes[name] 51 | if !exists { 52 | if tags.Optional[name] || val.Type().Field(fieldIdx).Type.Kind() == reflect.Ptr { 53 | // noop 54 | } else { 55 | diags = append(diags, &hcl.Diagnostic{ 56 | Severity: hcl.DiagError, 57 | Summary: fmt.Sprintf("Missing %s attribute", name), 58 | Detail: fmt.Sprintf("%s is required, but not defined here", name), 59 | }) 60 | } 61 | continue 62 | } 63 | diags = diags.Extend(gohcl.DecodeExpression(attr.Expr, ctx, val.Field(fieldIdx).Addr().Interface())) 64 | } 65 | 66 | blocksByType := body.Blocks.ByType() 67 | 68 | for typeName, fieldIdx := range tags.Blocks { 69 | blocks := blocksByType[typeName] 70 | field := val.Type().Field((fieldIdx)) 71 | 72 | ty := field.Type 73 | isSlice := false 74 | isPtr := false 75 | if ty.Kind() == reflect.Slice { 76 | isSlice = true 77 | ty = ty.Elem() 78 | } 79 | if ty.Kind() == reflect.Ptr { 80 | isPtr = true 81 | ty = ty.Elem() 82 | } 83 | 84 | if len(blocks) > 1 && !isSlice { 85 | diags = append(diags, &hcl.Diagnostic{ 86 | Severity: hcl.DiagError, 87 | Summary: fmt.Sprintf("Duplicate %s block", typeName), 88 | Detail: fmt.Sprintf( 89 | "Only one %s block is allowed. Another was defined at %s.", 90 | typeName, blocks[0].DefRange.String(), 91 | ), 92 | Subject: &blocks[1].DefRange, 93 | }) 94 | continue 95 | } 96 | 97 | if len(blocks) == 0 { 98 | if isSlice || isPtr { 99 | if val.Field(fieldIdx).IsNil() { 100 | val.Field(fieldIdx).Set(reflect.Zero(field.Type)) 101 | } 102 | } else { 103 | diags = append(diags, &hcl.Diagnostic{ 104 | Severity: hcl.DiagError, 105 | Summary: fmt.Sprintf("Missing %s block", typeName), 106 | Detail: fmt.Sprintf("A %s block is required.", typeName), 107 | }) 108 | } 109 | continue 110 | } 111 | 112 | switch { 113 | 114 | case isSlice: 115 | elemType := ty 116 | if isPtr { 117 | elemType = reflect.PtrTo(ty) 118 | } 119 | sli := val.Field(fieldIdx) 120 | if sli.IsNil() { 121 | sli = reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks)) 122 | } 123 | 124 | for i, block := range blocks { 125 | if isPtr { 126 | if i >= sli.Len() { 127 | sli = reflect.Append(sli, reflect.New(ty)) 128 | } 129 | v := sli.Index(i) 130 | if v.IsNil() { 131 | v = reflect.New(ty) 132 | } 133 | diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) 134 | sli.Index(i).Set(v) 135 | } else { 136 | if i >= sli.Len() { 137 | sli = reflect.Append(sli, reflect.Indirect(reflect.New(ty))) 138 | } 139 | diags = append(diags, decodeBlockToValue(block, ctx, sli.Index(i))...) 140 | } 141 | } 142 | 143 | if sli.Len() > len(blocks) { 144 | sli.SetLen(len(blocks)) 145 | } 146 | 147 | val.Field(fieldIdx).Set(sli) 148 | 149 | default: 150 | block := blocks[0] 151 | if isPtr { 152 | v := val.Field(fieldIdx) 153 | if v.IsNil() { 154 | v = reflect.New(ty) 155 | } 156 | diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) 157 | val.Field(fieldIdx).Set(v) 158 | } else { 159 | diags = append(diags, decodeBlockToValue(block, ctx, val.Field(fieldIdx))...) 160 | } 161 | 162 | } 163 | } 164 | 165 | return diags 166 | } 167 | 168 | func decodeBlockToValue(block *Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics { 169 | diags := decodeBody(block.Body, ctx, v) 170 | 171 | blockTags := getFieldTags(v.Type()) 172 | 173 | if len(block.Labels) > len(blockTags.Labels) { 174 | expectedLabels := make([]string, len(blockTags.Labels)) 175 | for i, label := range blockTags.Labels { 176 | expectedLabels[i] = label.Name 177 | } 178 | return append(diags, &hcl.Diagnostic{ 179 | Severity: hcl.DiagError, 180 | Summary: fmt.Sprintf("Extraneous label for %s", block.Type), 181 | Detail: fmt.Sprintf("Only %d labels (%s) are expected for %s blocks.", len(blockTags.Labels), strings.Join(expectedLabels, ", "), block.Type), 182 | Subject: &block.DefRange, 183 | }) 184 | } 185 | if len(block.Labels) < len(blockTags.Labels) { 186 | expectedLabels := make([]string, len(blockTags.Labels)) 187 | for i, label := range blockTags.Labels { 188 | expectedLabels[i] = label.Name 189 | } 190 | return append(diags, &hcl.Diagnostic{ 191 | Severity: hcl.DiagError, 192 | Summary: fmt.Sprintf("Missing label for %s", block.Type), 193 | Detail: fmt.Sprintf("All %s blocks must be have %d labels (%s).", block.Type, len(blockTags.Labels), strings.Join(expectedLabels, ", ")), 194 | Subject: &block.DefRange, 195 | }) 196 | } 197 | 198 | for li, lv := range block.Labels { 199 | lfieldIdx := blockTags.Labels[li].FieldIndex 200 | v.Field(lfieldIdx).Set(reflect.ValueOf(lv)) 201 | } 202 | 203 | return diags 204 | } 205 | -------------------------------------------------------------------------------- /plugin/internal/host2plugin/server.go: -------------------------------------------------------------------------------- 1 | package host2plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/go-plugin" 8 | "github.com/hashicorp/go-version" 9 | "github.com/terraform-linters/tflint-plugin-sdk/internal" 10 | "github.com/terraform-linters/tflint-plugin-sdk/logger" 11 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/fromproto" 12 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/interceptor" 13 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/plugin2host" 14 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/proto" 15 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/toproto" 16 | "github.com/terraform-linters/tflint-plugin-sdk/tflint" 17 | "google.golang.org/grpc" 18 | "google.golang.org/grpc/codes" 19 | "google.golang.org/grpc/status" 20 | ) 21 | 22 | // GRPCServer is a plugin-side implementation. Plugin must implement a server that returns a response for a request from host. 23 | // The behavior as gRPC server is implemented in the SDK, and the actual behavior is delegated to impl. 24 | type GRPCServer struct { 25 | proto.UnimplementedRuleSetServer 26 | 27 | impl tflint.RuleSet 28 | broker *plugin.GRPCBroker 29 | config *tflint.Config 30 | 31 | // TFLint v0.41 and earlier does not check version constraints, 32 | // so it returns an error in that case. 33 | constraintChecked bool 34 | } 35 | 36 | var _ proto.RuleSetServer = &GRPCServer{} 37 | 38 | // ServeOpts is an option for serving a plugin. 39 | // Each plugin can pass a RuleSet that represents its own functionality. 40 | type ServeOpts struct { 41 | RuleSet tflint.RuleSet 42 | } 43 | 44 | // Serve is a wrapper of plugin.Serve. This is entrypoint of all plugins. 45 | func Serve(opts *ServeOpts) { 46 | plugin.Serve(&plugin.ServeConfig{ 47 | HandshakeConfig: handshakeConfig, 48 | Plugins: map[string]plugin.Plugin{ 49 | "ruleset": &RuleSetPlugin{impl: opts.RuleSet}, 50 | }, 51 | GRPCServer: func(opts []grpc.ServerOption) *grpc.Server { 52 | opts = append(opts, grpc.UnaryInterceptor(interceptor.RequestLogging("host2plugin"))) 53 | return grpc.NewServer(opts...) 54 | }, 55 | Logger: logger.Logger(), 56 | }) 57 | } 58 | 59 | // GetName returns the name of the plugin. 60 | func (s *GRPCServer) GetName(ctx context.Context, req *proto.GetName_Request) (*proto.GetName_Response, error) { 61 | return &proto.GetName_Response{Name: s.impl.RuleSetName()}, nil 62 | } 63 | 64 | // GetVersion returns the version of the plugin. 65 | func (s *GRPCServer) GetVersion(ctx context.Context, req *proto.GetVersion_Request) (*proto.GetVersion_Response, error) { 66 | return &proto.GetVersion_Response{Version: s.impl.RuleSetVersion()}, nil 67 | } 68 | 69 | // GetRuleNames returns the list of rule names provided by the plugin. 70 | func (s *GRPCServer) GetRuleNames(ctx context.Context, req *proto.GetRuleNames_Request) (*proto.GetRuleNames_Response, error) { 71 | return &proto.GetRuleNames_Response{Names: s.impl.RuleNames()}, nil 72 | } 73 | 74 | // GetVersionConstraint returns a constraint of TFLint versions. 75 | func (s *GRPCServer) GetVersionConstraint(ctx context.Context, req *proto.GetVersionConstraint_Request) (*proto.GetVersionConstraint_Response, error) { 76 | s.constraintChecked = true 77 | constraints := version.Constraints{} 78 | var err error 79 | if s.impl.VersionConstraint() != "" { 80 | constraints, err = version.NewConstraint(s.impl.VersionConstraint()) 81 | if err != nil { 82 | return nil, status.Error(codes.Internal, err.Error()) 83 | } 84 | } 85 | // Append a minimum supported version constraint 86 | constraints = append(constraints, minTFLintVersionConstraint...) 87 | 88 | return &proto.GetVersionConstraint_Response{Constraint: constraints.String()}, nil 89 | } 90 | 91 | // GetSDKVersion returns the SDK version. 92 | func (s *GRPCServer) GetSDKVersion(ctx context.Context, req *proto.GetSDKVersion_Request) (*proto.GetSDKVersion_Response, error) { 93 | return &proto.GetSDKVersion_Response{Version: SDKVersion}, nil 94 | } 95 | 96 | // GetConfigSchema returns the config schema of the plugin. 97 | func (s *GRPCServer) GetConfigSchema(ctx context.Context, req *proto.GetConfigSchema_Request) (*proto.GetConfigSchema_Response, error) { 98 | return &proto.GetConfigSchema_Response{Schema: toproto.BodySchema(s.impl.ConfigSchema())}, nil 99 | } 100 | 101 | // ApplyGlobalConfig applies a common config to the plugin. 102 | func (s *GRPCServer) ApplyGlobalConfig(ctx context.Context, req *proto.ApplyGlobalConfig_Request) (*proto.ApplyGlobalConfig_Response, error) { 103 | // TFLint v0.41 and earlier does not check version constraints. 104 | if !s.constraintChecked { 105 | return nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("failed to satisfy version constraints; tflint-ruleset-%s requires %s, but TFLint version is 0.40 or 0.41", s.impl.RuleSetName(), minTFLintVersionConstraint)) 106 | } 107 | 108 | if req.Config == nil { 109 | return nil, status.Error(codes.InvalidArgument, "config should not be null") 110 | } 111 | 112 | s.config = fromproto.Config(req.Config) 113 | if err := s.impl.ApplyGlobalConfig(s.config); err != nil { 114 | return nil, toproto.Error(codes.FailedPrecondition, err) 115 | } 116 | return &proto.ApplyGlobalConfig_Response{}, nil 117 | } 118 | 119 | // ApplyConfig applies the plugin config retrieved from the host to the plugin. 120 | func (s *GRPCServer) ApplyConfig(ctx context.Context, req *proto.ApplyConfig_Request) (*proto.ApplyConfig_Response, error) { 121 | if req.Content == nil { 122 | return nil, status.Error(codes.InvalidArgument, "content should not be null") 123 | } 124 | 125 | content, diags := fromproto.BodyContent(req.Content) 126 | if diags.HasErrors() { 127 | return nil, toproto.Error(codes.InvalidArgument, diags) 128 | } 129 | if err := s.impl.ApplyConfig(content); err != nil { 130 | return nil, toproto.Error(codes.FailedPrecondition, err) 131 | } 132 | return &proto.ApplyConfig_Response{}, nil 133 | } 134 | 135 | // Check calls plugin rules with a gRPC client that can send requests 136 | // to the host process. 137 | func (s *GRPCServer) Check(ctx context.Context, req *proto.Check_Request) (*proto.Check_Response, error) { 138 | conn, err := s.broker.Dial(req.Runner) 139 | if err != nil { 140 | return nil, toproto.Error(codes.InvalidArgument, err) 141 | } 142 | defer conn.Close() 143 | 144 | client := proto.NewRunnerClient(conn) 145 | resp, err := client.GetFiles(ctx, &proto.GetFiles_Request{}) 146 | if err != nil { 147 | return nil, toproto.Error(codes.FailedPrecondition, err) 148 | } 149 | 150 | internalRunner := &plugin2host.GRPCClient{Client: client, Fixer: internal.NewFixer(resp.Files), FixEnabled: s.config.Fix} 151 | runner, err := s.impl.NewRunner(internalRunner) 152 | if err != nil { 153 | return nil, toproto.Error(codes.FailedPrecondition, err) 154 | } 155 | 156 | for _, rule := range s.impl.BuiltinImpl().EnabledRules { 157 | if err := rule.Check(runner); err != nil { 158 | return nil, toproto.Error(codes.Aborted, fmt.Errorf(`failed to check "%s" rule: %s`, rule.Name(), err)) 159 | } 160 | if internalRunner.Fixer.HasChanges() { 161 | internalRunner.Fixer.FormatChanges() 162 | if err := internalRunner.ApplyChanges(); err != nil { 163 | return nil, toproto.Error(codes.Aborted, fmt.Errorf(`failed to apply fixes by "%s" rule: %s`, rule.Name(), err)) 164 | } 165 | } 166 | } 167 | return &proto.Check_Response{}, nil 168 | } 169 | -------------------------------------------------------------------------------- /plugin/internal/plugin2host/server.go: -------------------------------------------------------------------------------- 1 | package plugin2host 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | "github.com/terraform-linters/tflint-plugin-sdk/hclext" 8 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/fromproto" 9 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/proto" 10 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/toproto" 11 | "github.com/terraform-linters/tflint-plugin-sdk/tflint" 12 | "github.com/zclconf/go-cty/cty" 13 | "github.com/zclconf/go-cty/cty/json" 14 | "google.golang.org/grpc/codes" 15 | "google.golang.org/grpc/status" 16 | ) 17 | 18 | // GRPCServer is a host-side implementation. Host must implement a server that returns a response for a request from plugin. 19 | // The behavior as gRPC server is implemented in the SDK, and the actual behavior is delegated to impl. 20 | type GRPCServer struct { 21 | proto.UnimplementedRunnerServer 22 | 23 | Impl Server 24 | } 25 | 26 | var _ proto.RunnerServer = &GRPCServer{} 27 | 28 | // Server is the interface that the host should implement when a plugin communicates with the host. 29 | type Server interface { 30 | GetOriginalwd() string 31 | GetModulePath() []string 32 | GetModuleContent(*hclext.BodySchema, tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) 33 | GetFile(string) (*hcl.File, error) 34 | // For performance, GetFiles returns map[string][]bytes instead of map[string]*hcl.File. 35 | GetFiles(tflint.ModuleCtxType) map[string][]byte 36 | GetRuleConfigContent(string, *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) 37 | EvaluateExpr(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) 38 | EmitIssue(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) 39 | ApplyChanges(map[string][]byte) error 40 | } 41 | 42 | // GetOriginalwd gets the original working directory. 43 | func (s *GRPCServer) GetOriginalwd(ctx context.Context, req *proto.GetOriginalwd_Request) (*proto.GetOriginalwd_Response, error) { 44 | return &proto.GetOriginalwd_Response{Path: s.Impl.GetOriginalwd()}, nil 45 | } 46 | 47 | // GetModulePath gets the current module path address. 48 | func (s *GRPCServer) GetModulePath(ctx context.Context, req *proto.GetModulePath_Request) (*proto.GetModulePath_Response, error) { 49 | return &proto.GetModulePath_Response{Path: s.Impl.GetModulePath()}, nil 50 | } 51 | 52 | // GetModuleContent gets the contents of the module based on the schema. 53 | func (s *GRPCServer) GetModuleContent(ctx context.Context, req *proto.GetModuleContent_Request) (*proto.GetModuleContent_Response, error) { 54 | if req.Schema == nil { 55 | return nil, status.Error(codes.InvalidArgument, "schema should not be null") 56 | } 57 | if req.Option == nil { 58 | return nil, status.Error(codes.InvalidArgument, "option should not be null") 59 | } 60 | 61 | opts := fromproto.GetModuleContentOption(req.Option) 62 | body, diags := s.Impl.GetModuleContent(fromproto.BodySchema(req.Schema), opts) 63 | if diags.HasErrors() { 64 | return nil, toproto.Error(codes.FailedPrecondition, diags) 65 | } 66 | if body == nil { 67 | return nil, status.Error(codes.FailedPrecondition, "response body is empty") 68 | } 69 | 70 | content := toproto.BodyContent(body, s.Impl.GetFiles(opts.ModuleCtx)) 71 | 72 | return &proto.GetModuleContent_Response{Content: content}, nil 73 | } 74 | 75 | // GetFile returns bytes of hcl.File based on the passed file name. 76 | func (s *GRPCServer) GetFile(ctx context.Context, req *proto.GetFile_Request) (*proto.GetFile_Response, error) { 77 | if req.Name == "" { 78 | return nil, status.Error(codes.InvalidArgument, "name should not be empty") 79 | } 80 | file, err := s.Impl.GetFile(req.Name) 81 | if err != nil { 82 | return nil, toproto.Error(codes.FailedPrecondition, err) 83 | } 84 | if file == nil { 85 | return nil, status.Error(codes.NotFound, "file not found") 86 | } 87 | return &proto.GetFile_Response{File: file.Bytes}, nil 88 | } 89 | 90 | // GetFiles returns bytes of hcl.File in the self module context. 91 | func (s *GRPCServer) GetFiles(ctx context.Context, req *proto.GetFiles_Request) (*proto.GetFiles_Response, error) { 92 | return &proto.GetFiles_Response{Files: s.Impl.GetFiles(tflint.SelfModuleCtxType)}, nil 93 | } 94 | 95 | // GetRuleConfigContent returns BodyContent based on the rule name and config schema. 96 | func (s *GRPCServer) GetRuleConfigContent(ctx context.Context, req *proto.GetRuleConfigContent_Request) (*proto.GetRuleConfigContent_Response, error) { 97 | if req.Name == "" { 98 | return nil, status.Error(codes.InvalidArgument, "name should not be empty") 99 | } 100 | if req.Schema == nil { 101 | return nil, status.Error(codes.InvalidArgument, "schema should not be null") 102 | } 103 | 104 | body, sources, err := s.Impl.GetRuleConfigContent(req.Name, fromproto.BodySchema(req.Schema)) 105 | if err != nil { 106 | return nil, toproto.Error(codes.FailedPrecondition, err) 107 | } 108 | if body == nil { 109 | return nil, status.Error(codes.FailedPrecondition, "response body is empty") 110 | } 111 | if len(sources) == 0 && !body.IsEmpty() { 112 | return nil, status.Error(codes.NotFound, "config file not found") 113 | } 114 | 115 | content := toproto.BodyContent(body, sources) 116 | return &proto.GetRuleConfigContent_Response{Content: content}, nil 117 | } 118 | 119 | // EvaluateExpr evals the passed expression based on the type. 120 | func (s *GRPCServer) EvaluateExpr(ctx context.Context, req *proto.EvaluateExpr_Request) (*proto.EvaluateExpr_Response, error) { 121 | if req.Expression == nil { 122 | return nil, status.Error(codes.InvalidArgument, "expression should not be null") 123 | } 124 | if req.Expression.Bytes == nil { 125 | return nil, status.Error(codes.InvalidArgument, "expression.bytes should not be null") 126 | } 127 | if req.Expression.Range == nil { 128 | return nil, status.Error(codes.InvalidArgument, "expression.range should not be null") 129 | } 130 | if req.Option == nil { 131 | return nil, status.Error(codes.InvalidArgument, "option should not be null") 132 | } 133 | 134 | expr, diags := fromproto.Expression(req.Expression) 135 | if diags.HasErrors() { 136 | return nil, toproto.Error(codes.InvalidArgument, diags) 137 | } 138 | ty, err := json.UnmarshalType(req.Option.Type) 139 | if err != nil { 140 | return nil, toproto.Error(codes.InvalidArgument, err) 141 | } 142 | 143 | value, err := s.Impl.EvaluateExpr(expr, tflint.EvaluateExprOption{WantType: &ty, ModuleCtx: fromproto.ModuleCtxType(req.Option.ModuleCtx)}) 144 | if err != nil { 145 | return nil, toproto.Error(codes.FailedPrecondition, err) 146 | } 147 | val, marks, err := toproto.Value(value, ty) 148 | if err != nil { 149 | return nil, toproto.Error(codes.FailedPrecondition, err) 150 | } 151 | 152 | return &proto.EvaluateExpr_Response{Value: val, Marks: marks}, nil 153 | } 154 | 155 | // EmitIssue emits the issue with the passed rule, message, location 156 | func (s *GRPCServer) EmitIssue(ctx context.Context, req *proto.EmitIssue_Request) (*proto.EmitIssue_Response, error) { 157 | if req.Rule == nil { 158 | return nil, status.Error(codes.InvalidArgument, "rule should not be null") 159 | } 160 | if req.Range == nil { 161 | return nil, status.Error(codes.InvalidArgument, "range should not be null") 162 | } 163 | 164 | applied, err := s.Impl.EmitIssue(fromproto.Rule(req.Rule), req.Message, fromproto.Range(req.Range), req.Fixable) 165 | if err != nil { 166 | return nil, toproto.Error(codes.FailedPrecondition, err) 167 | } 168 | return &proto.EmitIssue_Response{Applied: applied}, nil 169 | } 170 | 171 | // ApplyChanges applies the passed changes. 172 | func (s *GRPCServer) ApplyChanges(ctx context.Context, req *proto.ApplyChanges_Request) (*proto.ApplyChanges_Response, error) { 173 | err := s.Impl.ApplyChanges(req.Changes) 174 | if err != nil { 175 | return nil, toproto.Error(codes.InvalidArgument, err) 176 | } 177 | return &proto.ApplyChanges_Response{}, nil 178 | } 179 | -------------------------------------------------------------------------------- /plugin/internal/proto/tflint.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/proto"; 3 | 4 | package proto; 5 | 6 | service RuleSet { 7 | rpc GetName(GetName.Request) returns (GetName.Response); 8 | rpc GetVersion(GetVersion.Request) returns (GetVersion.Response); 9 | rpc GetVersionConstraint(GetVersionConstraint.Request) returns (GetVersionConstraint.Response); 10 | rpc GetSDKVersion(GetSDKVersion.Request) returns (GetSDKVersion.Response); 11 | rpc GetRuleNames(GetRuleNames.Request) returns (GetRuleNames.Response); 12 | rpc GetConfigSchema(GetConfigSchema.Request) returns (GetConfigSchema.Response); 13 | rpc ApplyGlobalConfig(ApplyGlobalConfig.Request) returns (ApplyGlobalConfig.Response); 14 | rpc ApplyConfig(ApplyConfig.Request) returns (ApplyConfig.Response); 15 | rpc Check(Check.Request) returns (Check.Response); 16 | } 17 | 18 | message GetName { 19 | message Request {} 20 | message Response { 21 | string name = 1; 22 | } 23 | } 24 | 25 | message GetVersion { 26 | message Request {} 27 | message Response { 28 | string version = 1; 29 | } 30 | } 31 | 32 | message GetVersionConstraint { 33 | message Request {} 34 | message Response { 35 | string constraint = 1; 36 | } 37 | } 38 | 39 | message GetSDKVersion { 40 | message Request {} 41 | message Response { 42 | string version = 1; 43 | } 44 | } 45 | 46 | message GetRuleNames { 47 | message Request {} 48 | message Response { 49 | repeated string names = 1; 50 | } 51 | } 52 | 53 | message GetConfigSchema { 54 | message Request {} 55 | message Response { 56 | BodySchema schema = 1; 57 | } 58 | } 59 | 60 | message ApplyGlobalConfig { 61 | message Config { 62 | map rules = 1; 63 | bool disabled_by_default = 2; 64 | repeated string only = 3; 65 | bool fix = 4; 66 | } 67 | message RuleConfig { 68 | string name = 1; 69 | bool enabled = 2; 70 | } 71 | 72 | message Request { 73 | Config config = 1; 74 | } 75 | message Response {} 76 | } 77 | 78 | message ApplyConfig { 79 | message Request { 80 | BodyContent content = 1; 81 | } 82 | message Response {} 83 | } 84 | 85 | message Check { 86 | message Request { 87 | uint32 runner = 1; 88 | } 89 | message Response {} 90 | } 91 | 92 | service Runner { 93 | rpc GetOriginalwd(GetOriginalwd.Request) returns (GetOriginalwd.Response); 94 | rpc GetModulePath(GetModulePath.Request) returns (GetModulePath.Response); 95 | rpc GetModuleContent(GetModuleContent.Request) returns (GetModuleContent.Response); 96 | rpc GetFile(GetFile.Request) returns (GetFile.Response); 97 | rpc GetFiles(GetFiles.Request) returns (GetFiles.Response); 98 | rpc GetRuleConfigContent(GetRuleConfigContent.Request) returns (GetRuleConfigContent.Response); 99 | rpc EvaluateExpr(EvaluateExpr.Request) returns (EvaluateExpr.Response); 100 | rpc EmitIssue(EmitIssue.Request) returns (EmitIssue.Response); 101 | rpc ApplyChanges(ApplyChanges.Request) returns (ApplyChanges.Response); 102 | } 103 | 104 | enum ModuleCtxType { 105 | MODULE_CTX_TYPE_UNSPECIFIED = 0; 106 | MODULE_CTX_TYPE_SELF = 1; 107 | MODULE_CTX_TYPE_ROOT = 2; 108 | } 109 | 110 | message GetOriginalwd { 111 | message Request {} 112 | message Response { 113 | string path = 1; 114 | } 115 | } 116 | 117 | message GetModulePath { 118 | message Request {} 119 | message Response { 120 | repeated string path = 1; 121 | } 122 | } 123 | 124 | message GetModuleContent { 125 | message Hint { 126 | string resource_type = 1; 127 | } 128 | enum ExpandMode { 129 | EXPAND_MODE_UNSPECIFIED = 0; 130 | EXPAND_MODE_NONE = 1; 131 | EXPAND_MODE_EXPAND = 2; 132 | } 133 | message Option { 134 | reserved 3; 135 | ModuleCtxType module_ctx = 1; 136 | Hint hint = 2; 137 | ExpandMode expand_mode = 4; 138 | } 139 | 140 | message Request { 141 | BodySchema schema = 1; 142 | Option option = 2; 143 | } 144 | message Response { 145 | BodyContent content = 1; 146 | } 147 | } 148 | 149 | message GetFile { 150 | message Request { 151 | string name = 1; 152 | } 153 | message Response { 154 | bytes file = 1; 155 | } 156 | } 157 | 158 | message GetFiles { 159 | message Request { 160 | } 161 | message Response { 162 | map files = 1; 163 | } 164 | } 165 | 166 | message GetRuleConfigContent { 167 | message Request { 168 | string name = 1; 169 | BodySchema schema = 2; 170 | } 171 | message Response { 172 | BodyContent content = 1; 173 | } 174 | } 175 | 176 | message EvaluateExpr { 177 | message Option { 178 | bytes type = 1; 179 | ModuleCtxType module_ctx = 2; 180 | } 181 | 182 | message Request { 183 | reserved 1, 2; 184 | Option option = 3; 185 | Expression expression = 4; 186 | } 187 | message Response { 188 | bytes value = 1; 189 | repeated ValueMark marks = 2; 190 | } 191 | } 192 | 193 | message EmitIssue { 194 | enum Severity { 195 | SEVERITY_UNSPECIFIED = 0; 196 | SEVERITY_ERROR = 1; 197 | SEVERITY_WARNING = 2; 198 | SEVERITY_NOTICE = 3; 199 | } 200 | message Rule { 201 | string name = 1; 202 | bool enabled = 2; 203 | Severity severity = 3; 204 | string link = 4; 205 | } 206 | 207 | message Request { 208 | Rule rule = 1; 209 | string message = 2; 210 | Range range = 3; 211 | bool fixable = 4; 212 | } 213 | message Response { 214 | bool applied = 1; 215 | } 216 | } 217 | 218 | message ApplyChanges { 219 | message Request { 220 | map changes = 1; 221 | } 222 | message Response {} 223 | } 224 | 225 | enum SchemaMode { 226 | SCHEMA_MODE_UNSPECIFIED = 0; 227 | SCHEMA_MODE_DEFAULT = 1; 228 | SCHEMA_MODE_JUST_ATTRIBUTES = 2; 229 | } 230 | 231 | message BodySchema { 232 | message Attribute { 233 | string name = 1; 234 | bool required = 2; 235 | } 236 | message Block { 237 | string type = 1; 238 | repeated string label_names = 2; 239 | BodySchema body = 3; 240 | } 241 | 242 | repeated Attribute attributes = 1; 243 | repeated Block blocks = 2; 244 | SchemaMode Mode = 3; 245 | } 246 | 247 | message BodyContent { 248 | message Attribute { 249 | reserved 2, 3; 250 | string name = 1; 251 | Range range = 4; 252 | Range name_range = 5; 253 | Expression expression = 6; 254 | } 255 | message Block { 256 | string type = 1; 257 | repeated string labels = 2; 258 | BodyContent body = 3; 259 | Range def_range = 4; 260 | Range type_range = 5; 261 | repeated Range label_ranges = 6; 262 | } 263 | 264 | map attributes = 1; 265 | repeated Block blocks = 2; 266 | } 267 | 268 | message Expression { 269 | bytes bytes = 1; 270 | Range range = 2; 271 | bytes value = 3; 272 | repeated ValueMark value_marks = 4; 273 | } 274 | 275 | message Range { 276 | message Pos { 277 | int64 line = 1; 278 | int64 column = 2; 279 | int64 byte = 3; 280 | } 281 | 282 | string filename = 1; 283 | Pos start = 2; 284 | Pos end = 3; 285 | } 286 | 287 | message AttributePath { 288 | message Step { 289 | oneof selector { 290 | string attribute_name = 1; 291 | string element_key_string = 2; 292 | int64 element_key_int = 3; 293 | } 294 | } 295 | repeated Step steps = 1; 296 | } 297 | 298 | message ValueMark { 299 | AttributePath path = 1; 300 | bool sensitive = 2; 301 | bool ephemeral = 3; 302 | } 303 | 304 | enum ErrorCode { 305 | reserved 1, 2, 3; 306 | ERROR_CODE_UNSPECIFIED = 0; 307 | ERROR_CODE_SENSITIVE = 4; 308 | } 309 | 310 | message ErrorDetail { 311 | ErrorCode code = 1; 312 | string message = 2; 313 | } 314 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= 2 | github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 3 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 4 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 5 | github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= 6 | github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 11 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 12 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 13 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 14 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 15 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 16 | github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= 17 | github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 18 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 19 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 20 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 21 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 22 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 23 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 24 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 25 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 26 | github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= 27 | github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= 28 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 29 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 30 | github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= 31 | github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= 32 | github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= 33 | github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= 34 | github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= 35 | github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= 36 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 37 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 38 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 39 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 40 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 41 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 42 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 43 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 44 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 45 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 46 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 47 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 48 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 50 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 51 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 52 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 53 | github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= 54 | github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= 55 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 56 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 57 | github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= 58 | github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= 59 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= 60 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= 61 | go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= 62 | go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 63 | go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= 64 | go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= 65 | go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= 66 | go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= 67 | go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= 68 | go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= 69 | go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= 70 | go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= 71 | go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= 72 | go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 73 | golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= 74 | golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 75 | golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= 76 | golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 77 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 78 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 79 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 80 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 82 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 83 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 84 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 85 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 86 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 87 | golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= 88 | golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 89 | golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= 90 | golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 91 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= 92 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 93 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= 94 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= 95 | google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= 96 | google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= 97 | google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= 98 | google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 99 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 100 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 101 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 102 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 103 | -------------------------------------------------------------------------------- /hclext/structure.go: -------------------------------------------------------------------------------- 1 | package hclext 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | ) 9 | 10 | // BodyContent is the result of applying a hclext.BodySchema to a hcl.Body. 11 | // Unlike hcl.BodyContent, this does not have MissingItemRange. 12 | // This difference is because hcl.BodyContent is the result for a single HCL file, 13 | // while hclext.BodyContent is the result for a Terraform module. 14 | type BodyContent struct { 15 | Attributes Attributes 16 | Blocks Blocks 17 | } 18 | 19 | // Blocks is a sequence of Block. 20 | type Blocks []*Block 21 | 22 | // Block represents a nested block within a hcl.Body. 23 | // Unlike hcl.Block, this has Body as hclext.BodyContent (struct), not hcl.Body (interface). 24 | // Since interface is hard to send over a wire protocol, it is designed to always return only the attributes based on the schema. 25 | // Instead, the hclext.BlockSchema can now be nested to extract the attributes within the nested block. 26 | type Block struct { 27 | Type string 28 | Labels []string 29 | Body *BodyContent 30 | 31 | DefRange hcl.Range 32 | TypeRange hcl.Range 33 | LabelRanges []hcl.Range 34 | } 35 | 36 | // Attributes is a set of attributes keyed by their names. 37 | // Please note that this is not strictly. Since hclext.BodyContent is the body from multiple files, 38 | // top-level attributes can have the same name (it is not possible to specify the same name within a block). 39 | // This exception is not considered here, as Terraform syntax does not allow top-level attributes. 40 | type Attributes map[string]*Attribute 41 | 42 | // Attribute represents an attribute from within a body. 43 | type Attribute struct { 44 | Name string 45 | Expr hcl.Expression 46 | 47 | Range hcl.Range 48 | NameRange hcl.Range 49 | } 50 | 51 | // Content is a wrapper for hcl.Content for working with nested schemas. 52 | // Convert hclext.BodySchema to hcl.BodySchema, and convert hcl.BodyContent 53 | // to hclext.BodyContent. It processes the nested body recursively. 54 | func Content(body hcl.Body, schema *BodySchema) (*BodyContent, hcl.Diagnostics) { 55 | if reflect.ValueOf(body).IsNil() { 56 | return &BodyContent{}, hcl.Diagnostics{} 57 | } 58 | if schema == nil { 59 | schema = &BodySchema{} 60 | } 61 | 62 | hclS := &hcl.BodySchema{ 63 | Attributes: make([]hcl.AttributeSchema, len(schema.Attributes)), 64 | Blocks: make([]hcl.BlockHeaderSchema, len(schema.Blocks)), 65 | } 66 | for idx, attrS := range schema.Attributes { 67 | hclS.Attributes[idx] = hcl.AttributeSchema{Name: attrS.Name, Required: attrS.Required} 68 | } 69 | childS := map[string]*BodySchema{} 70 | for idx, blockS := range schema.Blocks { 71 | hclS.Blocks[idx] = hcl.BlockHeaderSchema{Type: blockS.Type, LabelNames: blockS.LabelNames} 72 | childS[blockS.Type] = blockS.Body 73 | } 74 | 75 | content := &hcl.BodyContent{} 76 | var diags hcl.Diagnostics 77 | switch schema.Mode { 78 | case SchemaDefaultMode: 79 | content, diags = body.Content(hclS) 80 | case SchemaJustAttributesMode: 81 | content.Attributes, diags = body.JustAttributes() 82 | default: 83 | panic(fmt.Sprintf("invalid SchemaMode: %s", schema.Mode)) 84 | } 85 | 86 | ret := &BodyContent{ 87 | Attributes: Attributes{}, 88 | Blocks: make(Blocks, len(content.Blocks)), 89 | } 90 | for name, attr := range content.Attributes { 91 | ret.Attributes[name] = &Attribute{ 92 | Name: attr.Name, 93 | Expr: attr.Expr, 94 | Range: attr.Range, 95 | NameRange: attr.NameRange, 96 | } 97 | } 98 | for idx, block := range content.Blocks { 99 | child, childDiags := Content(block.Body, childS[block.Type]) 100 | diags = diags.Extend(childDiags) 101 | 102 | ret.Blocks[idx] = &Block{ 103 | Type: block.Type, 104 | Labels: block.Labels, 105 | Body: child, 106 | DefRange: block.DefRange, 107 | TypeRange: block.TypeRange, 108 | LabelRanges: block.LabelRanges, 109 | } 110 | } 111 | 112 | return ret, diags 113 | } 114 | 115 | // PartialContent is a wrapper for hcl.PartialContent for working with nested schemas. 116 | // Convert hclext.BodySchema to hcl.BodySchema, and convert hcl.BodyContent 117 | // to hclext.BodyContent. It processes the nested body recursively. 118 | // Unlike hcl.PartialContent, it does not return the rest of the body. 119 | func PartialContent(body hcl.Body, schema *BodySchema) (*BodyContent, hcl.Diagnostics) { 120 | if reflect.ValueOf(body).IsNil() { 121 | return &BodyContent{}, hcl.Diagnostics{} 122 | } 123 | if schema == nil { 124 | schema = &BodySchema{} 125 | } 126 | 127 | hclS := &hcl.BodySchema{ 128 | Attributes: make([]hcl.AttributeSchema, len(schema.Attributes)), 129 | Blocks: make([]hcl.BlockHeaderSchema, len(schema.Blocks)), 130 | } 131 | for idx, attrS := range schema.Attributes { 132 | hclS.Attributes[idx] = hcl.AttributeSchema{Name: attrS.Name, Required: attrS.Required} 133 | } 134 | childS := map[string]*BodySchema{} 135 | for idx, blockS := range schema.Blocks { 136 | hclS.Blocks[idx] = hcl.BlockHeaderSchema{Type: blockS.Type, LabelNames: blockS.LabelNames} 137 | childS[blockS.Type] = blockS.Body 138 | } 139 | 140 | content := &hcl.BodyContent{} 141 | var diags hcl.Diagnostics 142 | switch schema.Mode { 143 | case SchemaDefaultMode: 144 | content, _, diags = body.PartialContent(hclS) 145 | case SchemaJustAttributesMode: 146 | content.Attributes, diags = body.JustAttributes() 147 | default: 148 | panic(fmt.Sprintf("invalid SchemaMode: %s", schema.Mode)) 149 | } 150 | 151 | ret := &BodyContent{ 152 | Attributes: Attributes{}, 153 | Blocks: make(Blocks, len(content.Blocks)), 154 | } 155 | for name, attr := range content.Attributes { 156 | ret.Attributes[name] = &Attribute{ 157 | Name: attr.Name, 158 | Expr: attr.Expr, 159 | Range: attr.Range, 160 | NameRange: attr.NameRange, 161 | } 162 | } 163 | for idx, block := range content.Blocks { 164 | child, childDiags := PartialContent(block.Body, childS[block.Type]) 165 | diags = diags.Extend(childDiags) 166 | 167 | ret.Blocks[idx] = &Block{ 168 | Type: block.Type, 169 | Labels: block.Labels, 170 | Body: child, 171 | DefRange: block.DefRange, 172 | TypeRange: block.TypeRange, 173 | LabelRanges: block.LabelRanges, 174 | } 175 | } 176 | 177 | return ret, diags 178 | } 179 | 180 | // IsEmpty returns whether the body content is empty 181 | func (b *BodyContent) IsEmpty() bool { 182 | if b == nil { 183 | return true 184 | } 185 | return len(b.Attributes) == 0 && len(b.Blocks) == 0 186 | } 187 | 188 | // Copy returns a new BodyContent based on the original. 189 | func (b *BodyContent) Copy() *BodyContent { 190 | out := &BodyContent{ 191 | Attributes: Attributes{}, 192 | Blocks: make(Blocks, len(b.Blocks)), 193 | } 194 | 195 | for k, v := range b.Attributes { 196 | out.Attributes[k] = v.Copy() 197 | } 198 | copy(out.Blocks, b.Blocks) 199 | 200 | return out 201 | } 202 | 203 | // WalkAttributes visits all attributes with the passed walker function. 204 | func (b *BodyContent) WalkAttributes(walker func(*Attribute) hcl.Diagnostics) hcl.Diagnostics { 205 | var diags hcl.Diagnostics 206 | for _, attr := range b.Attributes { 207 | walkDiags := walker(attr) 208 | diags = diags.Extend(walkDiags) 209 | } 210 | for _, b := range b.Blocks { 211 | walkDiags := b.Body.WalkAttributes(walker) 212 | diags = diags.Extend(walkDiags) 213 | } 214 | return diags 215 | } 216 | 217 | // AsNative returns self as hcl.Attributes 218 | func (as Attributes) AsNative() hcl.Attributes { 219 | ret := hcl.Attributes{} 220 | for name, attr := range as { 221 | ret[name] = attr.AsNative() 222 | } 223 | return ret 224 | } 225 | 226 | // AsNative returns self as hcl.Attribute 227 | func (a *Attribute) AsNative() *hcl.Attribute { 228 | return &hcl.Attribute{ 229 | Name: a.Name, 230 | Expr: a.Expr, 231 | Range: a.Range, 232 | NameRange: a.NameRange, 233 | } 234 | } 235 | 236 | // Copy returns a new Attribute based on the original. 237 | // Note that expr can be a shallow copy. So strictly speaking 238 | // Copy is not a deep copy. 239 | func (a *Attribute) Copy() *Attribute { 240 | return &Attribute{ 241 | Name: a.Name, 242 | Expr: a.Expr, 243 | Range: a.Range, 244 | NameRange: a.NameRange, 245 | } 246 | } 247 | 248 | // OfType filters the receiving block sequence by block type name, 249 | // returning a new block sequence including only the blocks of the 250 | // requested type. 251 | func (els Blocks) OfType(typeName string) Blocks { 252 | ret := make(Blocks, 0) 253 | for _, el := range els { 254 | if el.Type == typeName { 255 | ret = append(ret, el) 256 | } 257 | } 258 | return ret 259 | } 260 | 261 | // ByType transforms the receiving block sequence into a map from type 262 | // name to block sequences of only that type. 263 | func (els Blocks) ByType() map[string]Blocks { 264 | ret := make(map[string]Blocks) 265 | for _, el := range els { 266 | ty := el.Type 267 | if ret[ty] == nil { 268 | ret[ty] = make(Blocks, 0, 1) 269 | } 270 | ret[ty] = append(ret[ty], el) 271 | } 272 | return ret 273 | } 274 | 275 | // Copy returns a new Block based on the original. 276 | func (b *Block) Copy() *Block { 277 | out := &Block{ 278 | Type: b.Type, 279 | Labels: make([]string, len(b.Labels)), 280 | Body: b.Body.Copy(), 281 | DefRange: b.DefRange, 282 | TypeRange: b.TypeRange, 283 | LabelRanges: make([]hcl.Range, len(b.LabelRanges)), 284 | } 285 | 286 | copy(out.Labels, b.Labels) 287 | copy(out.LabelRanges, b.LabelRanges) 288 | 289 | return out 290 | } 291 | -------------------------------------------------------------------------------- /plugin/internal/toproto/toproto.go: -------------------------------------------------------------------------------- 1 | package toproto 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/terraform-linters/tflint-plugin-sdk/hclext" 9 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/proto" 10 | "github.com/terraform-linters/tflint-plugin-sdk/terraform/lang/marks" 11 | "github.com/terraform-linters/tflint-plugin-sdk/tflint" 12 | "github.com/zclconf/go-cty/cty" 13 | "github.com/zclconf/go-cty/cty/convert" 14 | "github.com/zclconf/go-cty/cty/msgpack" 15 | "google.golang.org/grpc/codes" 16 | "google.golang.org/grpc/status" 17 | ) 18 | 19 | // BodySchema converts schema.BodySchema to proto.BodySchema 20 | func BodySchema(body *hclext.BodySchema) *proto.BodySchema { 21 | if body == nil { 22 | return &proto.BodySchema{} 23 | } 24 | 25 | attributes := make([]*proto.BodySchema_Attribute, len(body.Attributes)) 26 | for idx, attr := range body.Attributes { 27 | attributes[idx] = &proto.BodySchema_Attribute{Name: attr.Name, Required: attr.Required} 28 | } 29 | 30 | blocks := make([]*proto.BodySchema_Block, len(body.Blocks)) 31 | for idx, block := range body.Blocks { 32 | blocks[idx] = &proto.BodySchema_Block{ 33 | Type: block.Type, 34 | LabelNames: block.LabelNames, 35 | Body: BodySchema(block.Body), 36 | } 37 | } 38 | 39 | return &proto.BodySchema{ 40 | Mode: SchemaMode(body.Mode), 41 | Attributes: attributes, 42 | Blocks: blocks, 43 | } 44 | } 45 | 46 | // SchemaMode converts hclext.SchemaMode to proto.SchemaMode 47 | func SchemaMode(mode hclext.SchemaMode) proto.SchemaMode { 48 | switch mode { 49 | case hclext.SchemaDefaultMode: 50 | return proto.SchemaMode_SCHEMA_MODE_DEFAULT 51 | case hclext.SchemaJustAttributesMode: 52 | return proto.SchemaMode_SCHEMA_MODE_JUST_ATTRIBUTES 53 | default: 54 | panic(fmt.Sprintf("invalid SchemaMode: %s", mode)) 55 | } 56 | } 57 | 58 | // BodyContent converts schema.BodyContent to proto.BodyContent 59 | func BodyContent(body *hclext.BodyContent, sources map[string][]byte) *proto.BodyContent { 60 | if body == nil { 61 | return &proto.BodyContent{} 62 | } 63 | 64 | attributes := map[string]*proto.BodyContent_Attribute{} 65 | for idx, attr := range body.Attributes { 66 | bytes, ok := sources[attr.Range.Filename] 67 | if !ok { 68 | panic(fmt.Sprintf("failed to encode to protocol buffers: source code not available: name=%s", attr.Range.Filename)) 69 | } 70 | 71 | attributes[idx] = &proto.BodyContent_Attribute{ 72 | Name: attr.Name, 73 | Expression: Expression(attr.Expr, bytes), 74 | Range: Range(attr.Range), 75 | NameRange: Range(attr.NameRange), 76 | } 77 | } 78 | 79 | blocks := make([]*proto.BodyContent_Block, len(body.Blocks)) 80 | for idx, block := range body.Blocks { 81 | labelRanges := make([]*proto.Range, len(block.LabelRanges)) 82 | for idx, labelRange := range block.LabelRanges { 83 | labelRanges[idx] = Range(labelRange) 84 | } 85 | 86 | blocks[idx] = &proto.BodyContent_Block{ 87 | Type: block.Type, 88 | Labels: block.Labels, 89 | Body: BodyContent(block.Body, sources), 90 | DefRange: Range(block.DefRange), 91 | TypeRange: Range(block.TypeRange), 92 | LabelRanges: labelRanges, 93 | } 94 | } 95 | 96 | return &proto.BodyContent{ 97 | Attributes: attributes, 98 | Blocks: blocks, 99 | } 100 | } 101 | 102 | // Rule converts tflint.Rule to proto.EmitIssue_Rule 103 | func Rule(rule tflint.Rule) *proto.EmitIssue_Rule { 104 | if rule == nil { 105 | panic("failed to encode to protocol buffers: rule should not be nil") 106 | } 107 | return &proto.EmitIssue_Rule{ 108 | Name: rule.Name(), 109 | Enabled: rule.Enabled(), 110 | Severity: Severity(rule.Severity()), 111 | Link: rule.Link(), 112 | } 113 | } 114 | 115 | // Expression converts hcl.Expression to proto.Expression 116 | func Expression(expr hcl.Expression, source []byte) *proto.Expression { 117 | out := &proto.Expression{ 118 | Bytes: expr.Range().SliceBytes(source), 119 | Range: Range(expr.Range()), 120 | } 121 | 122 | if boundExpr, ok := expr.(*hclext.BoundExpr); ok { 123 | val, marks, err := Value(boundExpr.Val, cty.DynamicPseudoType) 124 | if err != nil { 125 | panic(fmt.Errorf("cannot marshal the bound expr: %w", err)) 126 | } 127 | out.Value = val 128 | out.ValueMarks = marks 129 | } 130 | return out 131 | } 132 | 133 | // Severity converts severity to proto.EmitIssue_Severity 134 | func Severity(severity tflint.Severity) proto.EmitIssue_Severity { 135 | switch severity { 136 | case tflint.ERROR: 137 | return proto.EmitIssue_SEVERITY_ERROR 138 | case tflint.WARNING: 139 | return proto.EmitIssue_SEVERITY_WARNING 140 | case tflint.NOTICE: 141 | return proto.EmitIssue_SEVERITY_NOTICE 142 | } 143 | 144 | return proto.EmitIssue_SEVERITY_ERROR 145 | } 146 | 147 | // Range converts hcl.Range to proto.Range 148 | func Range(rng hcl.Range) *proto.Range { 149 | return &proto.Range{ 150 | Filename: rng.Filename, 151 | Start: Pos(rng.Start), 152 | End: Pos(rng.End), 153 | } 154 | } 155 | 156 | // Pos converts hcl.Pos to proto.Range_Pos 157 | func Pos(pos hcl.Pos) *proto.Range_Pos { 158 | return &proto.Range_Pos{ 159 | Line: int64(pos.Line), 160 | Column: int64(pos.Column), 161 | Byte: int64(pos.Byte), 162 | } 163 | } 164 | 165 | // Value converts cty.Value to msgpack and serialized value marks 166 | func Value(value cty.Value, ty cty.Type) ([]byte, []*proto.ValueMark, error) { 167 | // Convert first to get the actual cty.Path 168 | value, err := convert.Convert(value, ty) 169 | if err != nil { 170 | return nil, nil, err 171 | } 172 | 173 | value, pvm := value.UnmarkDeepWithPaths() 174 | valueMarks := make([]*proto.ValueMark, len(pvm)) 175 | for idx, m := range pvm { 176 | path, err := AttributePath(m.Path) 177 | if err != nil { 178 | return nil, nil, err 179 | } 180 | 181 | valueMarks[idx] = &proto.ValueMark{Path: path} 182 | if _, exists := m.Marks[marks.Sensitive]; exists { 183 | valueMarks[idx].Sensitive = true 184 | } 185 | if _, exists := m.Marks[marks.Ephemeral]; exists { 186 | valueMarks[idx].Ephemeral = true 187 | } 188 | } 189 | 190 | val, err := msgpack.Marshal(value, ty) 191 | if err != nil { 192 | return nil, nil, err 193 | } 194 | 195 | return val, valueMarks, nil 196 | } 197 | 198 | // AttributePath converts cty.Path to proto.AttributePath 199 | func AttributePath(path cty.Path) (*proto.AttributePath, error) { 200 | steps := make([]*proto.AttributePath_Step, len(path)) 201 | 202 | for idx, step := range path { 203 | switch s := step.(type) { 204 | case cty.IndexStep: 205 | switch s.Key.Type() { 206 | case cty.String: 207 | steps[idx] = &proto.AttributePath_Step{ 208 | Selector: &proto.AttributePath_Step_ElementKeyString{ElementKeyString: s.Key.AsString()}, 209 | } 210 | case cty.Number: 211 | v, _ := s.Key.AsBigFloat().Int64() 212 | steps[idx] = &proto.AttributePath_Step{ 213 | Selector: &proto.AttributePath_Step_ElementKeyInt{ElementKeyInt: v}, 214 | } 215 | default: 216 | return nil, fmt.Errorf("unknown index step key type: %s", s.Key.Type().GoString()) 217 | } 218 | case cty.GetAttrStep: 219 | steps[idx] = &proto.AttributePath_Step{ 220 | Selector: &proto.AttributePath_Step_AttributeName{AttributeName: s.Name}, 221 | } 222 | default: 223 | return nil, fmt.Errorf("unknown attribute path step: %T", s) 224 | } 225 | } 226 | 227 | return &proto.AttributePath{Steps: steps}, nil 228 | } 229 | 230 | // Config converts tflint.Config to proto.ApplyGlobalConfig_Config 231 | func Config(config *tflint.Config) *proto.ApplyGlobalConfig_Config { 232 | if config == nil { 233 | return &proto.ApplyGlobalConfig_Config{Rules: make(map[string]*proto.ApplyGlobalConfig_RuleConfig)} 234 | } 235 | 236 | rules := map[string]*proto.ApplyGlobalConfig_RuleConfig{} 237 | for name, rule := range config.Rules { 238 | rules[name] = &proto.ApplyGlobalConfig_RuleConfig{Name: rule.Name, Enabled: rule.Enabled} 239 | } 240 | return &proto.ApplyGlobalConfig_Config{ 241 | Rules: rules, 242 | DisabledByDefault: config.DisabledByDefault, 243 | Only: config.Only, 244 | Fix: config.Fix, 245 | } 246 | } 247 | 248 | // GetModuleContentOption converts tflint.GetModuleContentOption to proto.GetModuleContent_Option 249 | func GetModuleContentOption(opts *tflint.GetModuleContentOption) *proto.GetModuleContent_Option { 250 | if opts == nil { 251 | return &proto.GetModuleContent_Option{} 252 | } 253 | 254 | return &proto.GetModuleContent_Option{ 255 | ModuleCtx: ModuleCtxType(opts.ModuleCtx), 256 | ExpandMode: ExpandMode(opts.ExpandMode), 257 | Hint: GetModuleContentHint(opts.Hint), 258 | } 259 | } 260 | 261 | // ModuleCtxType converts tflint.ModuleCtxType to proto.ModuleCtxType 262 | func ModuleCtxType(ty tflint.ModuleCtxType) proto.ModuleCtxType { 263 | switch ty { 264 | case tflint.SelfModuleCtxType: 265 | return proto.ModuleCtxType_MODULE_CTX_TYPE_SELF 266 | case tflint.RootModuleCtxType: 267 | return proto.ModuleCtxType_MODULE_CTX_TYPE_ROOT 268 | default: 269 | panic(fmt.Sprintf("invalid ModuleCtxType: %s", ty.String())) 270 | } 271 | } 272 | 273 | // ExpandMode converts tflint.ExpandMode to proto.GetModuleContent_ExpandMode 274 | func ExpandMode(mode tflint.ExpandMode) proto.GetModuleContent_ExpandMode { 275 | switch mode { 276 | case tflint.ExpandModeExpand: 277 | return proto.GetModuleContent_EXPAND_MODE_EXPAND 278 | case tflint.ExpandModeNone: 279 | return proto.GetModuleContent_EXPAND_MODE_NONE 280 | default: 281 | panic(fmt.Sprintf("invalid ExpandMode: %s", mode)) 282 | } 283 | } 284 | 285 | // GetModuleContentHint converts tflint.GetModuleContentHint to proto.GetModuleContentHint 286 | func GetModuleContentHint(hint tflint.GetModuleContentHint) *proto.GetModuleContent_Hint { 287 | return &proto.GetModuleContent_Hint{ 288 | ResourceType: hint.ResourceType, 289 | } 290 | } 291 | 292 | // Error converts error to gRPC error status with details 293 | func Error(code codes.Code, err error) error { 294 | if err == nil { 295 | return nil 296 | } 297 | 298 | var errCode proto.ErrorCode 299 | if errors.Is(err, tflint.ErrSensitive) { 300 | errCode = proto.ErrorCode_ERROR_CODE_SENSITIVE 301 | } 302 | 303 | if errCode == proto.ErrorCode_ERROR_CODE_UNSPECIFIED { 304 | return status.Error(code, err.Error()) 305 | } 306 | 307 | st := status.New(code, err.Error()) 308 | dt, err := st.WithDetails(&proto.ErrorDetail{Code: errCode}) 309 | if err != nil { 310 | return status.Error(codes.Unknown, fmt.Sprintf("Failed to add ErrorDetail: code=%d error=%s", code, err.Error())) 311 | } 312 | 313 | return dt.Err() 314 | } 315 | -------------------------------------------------------------------------------- /internal/token_scanner_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | "github.com/google/go-cmp/cmp/cmpopts" 8 | "github.com/hashicorp/hcl/v2" 9 | "github.com/hashicorp/hcl/v2/hclsyntax" 10 | ) 11 | 12 | func TestSeek(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | source string 16 | seek func(*tokenScanner) error 17 | wantPos hcl.Pos 18 | wantToken hclsyntax.Token 19 | }{ 20 | { 21 | name: "seek to initial position", 22 | source: `foo = 1`, 23 | seek: func(s *tokenScanner) error { 24 | return s.seek(hcl.InitialPos, tokenStart) 25 | }, 26 | wantPos: hcl.InitialPos, 27 | wantToken: hclsyntax.Token{ 28 | Type: hclsyntax.TokenIdent, 29 | Bytes: []byte("foo"), 30 | }, 31 | }, 32 | { 33 | name: "seek to forward with tokenStart", 34 | source: `foo = 1`, 35 | seek: func(s *tokenScanner) error { 36 | return s.seek(hcl.Pos{Line: 1, Column: 7, Byte: 6}, tokenStart) 37 | }, 38 | wantPos: hcl.Pos{Line: 1, Column: 7, Byte: 6}, 39 | wantToken: hclsyntax.Token{ 40 | Type: hclsyntax.TokenNumberLit, 41 | Bytes: []byte("1"), 42 | }, 43 | }, 44 | { 45 | name: "seek to forward with tokenEnd", 46 | source: `foo = 1`, 47 | seek: func(s *tokenScanner) error { 48 | return s.seek(hcl.Pos{Line: 1, Column: 8, Byte: 7}, tokenEnd) 49 | }, 50 | wantPos: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 51 | wantToken: hclsyntax.Token{ 52 | Type: hclsyntax.TokenNumberLit, 53 | Bytes: []byte("1"), 54 | }, 55 | }, 56 | { 57 | name: "seek to backward with tokenStart", 58 | source: `foo = 1`, 59 | seek: func(s *tokenScanner) error { 60 | s.seek(hcl.Pos{Line: 1, Column: 7, Byte: 6}, tokenStart) 61 | return s.seek(hcl.Pos{Line: 1, Column: 5, Byte: 4}, tokenStart) 62 | }, 63 | wantPos: hcl.Pos{Line: 1, Column: 5, Byte: 4}, 64 | wantToken: hclsyntax.Token{ 65 | Type: hclsyntax.TokenEqual, 66 | Bytes: []byte("="), 67 | }, 68 | }, 69 | { 70 | name: "seek to backward with tokenEnd", 71 | source: `foo = 1`, 72 | seek: func(s *tokenScanner) error { 73 | s.seek(hcl.Pos{Line: 1, Column: 7, Byte: 6}, tokenStart) 74 | return s.seek(hcl.Pos{Line: 1, Column: 6, Byte: 5}, tokenEnd) 75 | }, 76 | wantPos: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 77 | wantToken: hclsyntax.Token{ 78 | Type: hclsyntax.TokenEqual, 79 | Bytes: []byte("="), 80 | }, 81 | }, 82 | } 83 | 84 | for _, test := range tests { 85 | t.Run(test.name, func(t *testing.T) { 86 | scanner, diags := newTokenScanner([]byte(test.source), "main.tf") 87 | if diags.HasErrors() { 88 | t.Fatalf("failed to set up token scanner: %s", diags) 89 | } 90 | if err := test.seek(scanner); err != nil { 91 | t.Fatalf("failed to seek: %s", err) 92 | } 93 | 94 | if diff := cmp.Diff(test.wantPos, scanner.pos); diff != "" { 95 | t.Errorf("position mismatch: %s", diff) 96 | } 97 | opt := cmpopts.IgnoreFields(hclsyntax.Token{}, "Range") 98 | if diff := cmp.Diff(test.wantToken, scanner.token(), opt); diff != "" { 99 | t.Errorf("token mismatch: %s", diff) 100 | } 101 | }) 102 | } 103 | } 104 | 105 | func TestScan(t *testing.T) { 106 | type scanResult struct { 107 | Pos hcl.Pos 108 | Token hclsyntax.Token 109 | } 110 | 111 | tests := []struct { 112 | name string 113 | source string 114 | seek func(*tokenScanner) error 115 | scanResults []scanResult 116 | want hcl.Pos 117 | }{ 118 | { 119 | name: "scan all tokens", 120 | source: `foo = 1`, 121 | seek: func(s *tokenScanner) error { return nil }, 122 | scanResults: []scanResult{ 123 | { 124 | Pos: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 125 | Token: hclsyntax.Token{ 126 | Type: hclsyntax.TokenEqual, 127 | Bytes: []byte("="), 128 | }, 129 | }, 130 | { 131 | Pos: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 132 | Token: hclsyntax.Token{ 133 | Type: hclsyntax.TokenNumberLit, 134 | Bytes: []byte("1"), 135 | }, 136 | }, 137 | { 138 | Pos: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 139 | Token: hclsyntax.Token{ 140 | Type: hclsyntax.TokenEOF, 141 | Bytes: []byte{}, 142 | }, 143 | }, 144 | }, 145 | want: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 146 | }, 147 | { 148 | name: "scan tokens from the middle", 149 | source: `foo = 1`, 150 | seek: func(s *tokenScanner) error { 151 | return s.seek(hcl.Pos{Line: 1, Column: 5, Byte: 4}, tokenStart) 152 | }, 153 | scanResults: []scanResult{ 154 | { 155 | Pos: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 156 | Token: hclsyntax.Token{ 157 | Type: hclsyntax.TokenNumberLit, 158 | Bytes: []byte("1"), 159 | }, 160 | }, 161 | { 162 | Pos: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 163 | Token: hclsyntax.Token{ 164 | Type: hclsyntax.TokenEOF, 165 | Bytes: []byte{}, 166 | }, 167 | }, 168 | }, 169 | want: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 170 | }, 171 | { 172 | name: "scan tokens from tokenEnd", 173 | source: `foo = 1`, 174 | seek: func(s *tokenScanner) error { 175 | return s.seek(hcl.Pos{Line: 1, Column: 6, Byte: 5}, tokenEnd) 176 | }, 177 | scanResults: []scanResult{ 178 | { 179 | Pos: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 180 | Token: hclsyntax.Token{ 181 | Type: hclsyntax.TokenNumberLit, 182 | Bytes: []byte("1"), 183 | }, 184 | }, 185 | { 186 | Pos: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 187 | Token: hclsyntax.Token{ 188 | Type: hclsyntax.TokenEOF, 189 | Bytes: []byte{}, 190 | }, 191 | }, 192 | }, 193 | want: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 194 | }, 195 | { 196 | name: "no scan", 197 | source: `foo = 1`, 198 | seek: func(s *tokenScanner) error { 199 | return s.seek(hcl.Pos{Line: 1, Column: 8, Byte: 7}, tokenStart) 200 | }, 201 | scanResults: []scanResult{}, 202 | want: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 203 | }, 204 | } 205 | 206 | for _, test := range tests { 207 | t.Run(test.name, func(t *testing.T) { 208 | scanner, diags := newTokenScanner([]byte(test.source), "main.tf") 209 | if diags.HasErrors() { 210 | t.Fatalf("failed to set up token scanner: %s", diags) 211 | } 212 | if err := test.seek(scanner); err != nil { 213 | t.Fatalf("failed to seek: %s", err) 214 | } 215 | 216 | scanResults := []scanResult{} 217 | for scanner.scan() { 218 | scanResults = append(scanResults, scanResult{ 219 | Pos: scanner.pos, 220 | Token: scanner.token(), 221 | }) 222 | } 223 | 224 | opt := cmpopts.IgnoreFields(hclsyntax.Token{}, "Range") 225 | if diff := cmp.Diff(test.scanResults, scanResults, opt); diff != "" { 226 | t.Errorf("scan result mismatch: %s", diff) 227 | } 228 | if diff := cmp.Diff(test.want, scanner.pos); diff != "" { 229 | t.Errorf("position mismatch: %s", diff) 230 | } 231 | }) 232 | } 233 | } 234 | 235 | func TestScanBackward(t *testing.T) { 236 | type scanResult struct { 237 | Pos hcl.Pos 238 | Token hclsyntax.Token 239 | } 240 | 241 | tests := []struct { 242 | name string 243 | source string 244 | seek func(*tokenScanner) error 245 | scanResults []scanResult 246 | want hcl.Pos 247 | }{ 248 | { 249 | name: "scan all tokens", 250 | source: `foo = 1`, 251 | seek: func(s *tokenScanner) error { 252 | return s.seek(hcl.Pos{Line: 1, Column: 8, Byte: 7}, tokenStart) 253 | }, 254 | scanResults: []scanResult{ 255 | { 256 | Pos: hcl.Pos{Line: 1, Column: 7, Byte: 6}, 257 | Token: hclsyntax.Token{ 258 | Type: hclsyntax.TokenNumberLit, 259 | Bytes: []byte("1"), 260 | }, 261 | }, 262 | { 263 | Pos: hcl.Pos{Line: 1, Column: 5, Byte: 4}, 264 | Token: hclsyntax.Token{ 265 | Type: hclsyntax.TokenEqual, 266 | Bytes: []byte("="), 267 | }, 268 | }, 269 | { 270 | Pos: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 271 | Token: hclsyntax.Token{ 272 | Type: hclsyntax.TokenIdent, 273 | Bytes: []byte("foo"), 274 | }, 275 | }, 276 | }, 277 | want: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 278 | }, 279 | { 280 | name: "scan tokens from the middle", 281 | source: `foo = 1`, 282 | seek: func(s *tokenScanner) error { 283 | return s.seek(hcl.Pos{Line: 1, Column: 6, Byte: 5}, tokenEnd) 284 | }, 285 | scanResults: []scanResult{ 286 | { 287 | Pos: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 288 | Token: hclsyntax.Token{ 289 | Type: hclsyntax.TokenIdent, 290 | Bytes: []byte("foo"), 291 | }, 292 | }, 293 | }, 294 | want: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 295 | }, 296 | { 297 | name: "scan tokens from tokenStart", 298 | source: `foo = 1`, 299 | seek: func(s *tokenScanner) error { 300 | return s.seek(hcl.Pos{Line: 1, Column: 5, Byte: 4}, tokenStart) 301 | }, 302 | scanResults: []scanResult{ 303 | { 304 | Pos: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 305 | Token: hclsyntax.Token{ 306 | Type: hclsyntax.TokenIdent, 307 | Bytes: []byte("foo"), 308 | }, 309 | }, 310 | }, 311 | want: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 312 | }, 313 | { 314 | name: "no scan", 315 | source: `foo = 1`, 316 | seek: func(s *tokenScanner) error { return nil }, 317 | scanResults: []scanResult{}, 318 | want: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 319 | }, 320 | { 321 | name: "no scan from endToken", 322 | source: `foo = 1`, 323 | seek: func(s *tokenScanner) error { 324 | s.seekTokenEnd() 325 | return nil 326 | }, 327 | scanResults: []scanResult{}, 328 | want: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 329 | }, 330 | } 331 | 332 | for _, test := range tests { 333 | t.Run(test.name, func(t *testing.T) { 334 | scanner, diags := newTokenScanner([]byte(test.source), "main.tf") 335 | if diags.HasErrors() { 336 | t.Fatalf("failed to set up token scanner: %s", diags) 337 | } 338 | if err := test.seek(scanner); err != nil { 339 | t.Fatalf("failed to seek: %s", err) 340 | } 341 | 342 | scanResults := []scanResult{} 343 | for scanner.scanBackward() { 344 | scanResults = append(scanResults, scanResult{ 345 | Pos: scanner.pos, 346 | Token: scanner.token(), 347 | }) 348 | } 349 | 350 | opt := cmpopts.IgnoreFields(hclsyntax.Token{}, "Range") 351 | if diff := cmp.Diff(test.scanResults, scanResults, opt); diff != "" { 352 | t.Errorf("scan result mismatch: %s", diff) 353 | } 354 | if diff := cmp.Diff(test.want, scanner.pos); diff != "" { 355 | t.Errorf("position mismatch: %s", diff) 356 | } 357 | }) 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /plugin/internal/fromproto/fromproto.go: -------------------------------------------------------------------------------- 1 | package fromproto 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/terraform-linters/tflint-plugin-sdk/hclext" 9 | "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/proto" 10 | "github.com/terraform-linters/tflint-plugin-sdk/terraform/lang/marks" 11 | "github.com/terraform-linters/tflint-plugin-sdk/tflint" 12 | "github.com/zclconf/go-cty/cty" 13 | "github.com/zclconf/go-cty/cty/msgpack" 14 | "google.golang.org/grpc/codes" 15 | "google.golang.org/grpc/status" 16 | ) 17 | 18 | // BodySchema converts proto.BodySchema to hclext.BodySchema 19 | func BodySchema(body *proto.BodySchema) *hclext.BodySchema { 20 | if body == nil { 21 | return nil 22 | } 23 | 24 | attributes := make([]hclext.AttributeSchema, len(body.Attributes)) 25 | for idx, attr := range body.Attributes { 26 | attributes[idx] = hclext.AttributeSchema{Name: attr.Name, Required: attr.Required} 27 | } 28 | 29 | blocks := make([]hclext.BlockSchema, len(body.Blocks)) 30 | for idx, block := range body.Blocks { 31 | blocks[idx] = hclext.BlockSchema{ 32 | Type: block.Type, 33 | LabelNames: block.LabelNames, 34 | Body: BodySchema(block.Body), 35 | } 36 | } 37 | 38 | return &hclext.BodySchema{ 39 | Mode: SchemaMode(body.Mode), 40 | Attributes: attributes, 41 | Blocks: blocks, 42 | } 43 | } 44 | 45 | // SchemaMode converts proto.SchemaMode to hclext.SchemaMode 46 | func SchemaMode(mode proto.SchemaMode) hclext.SchemaMode { 47 | switch mode { 48 | case proto.SchemaMode_SCHEMA_MODE_UNSPECIFIED: 49 | return hclext.SchemaDefaultMode 50 | case proto.SchemaMode_SCHEMA_MODE_DEFAULT: 51 | return hclext.SchemaDefaultMode 52 | case proto.SchemaMode_SCHEMA_MODE_JUST_ATTRIBUTES: 53 | return hclext.SchemaJustAttributesMode 54 | default: 55 | panic(fmt.Sprintf("invalid SchemaMode: %s", mode)) 56 | } 57 | } 58 | 59 | // BodyContent converts proto.BodyContent to hclext.BodyContent 60 | func BodyContent(body *proto.BodyContent) (*hclext.BodyContent, hcl.Diagnostics) { 61 | if body == nil { 62 | return nil, nil 63 | } 64 | diags := hcl.Diagnostics{} 65 | 66 | attributes := hclext.Attributes{} 67 | for key, attr := range body.Attributes { 68 | expr, exprDiags := Expression(attr.Expression) 69 | diags = diags.Extend(exprDiags) 70 | 71 | attributes[key] = &hclext.Attribute{ 72 | Name: attr.Name, 73 | Expr: expr, 74 | Range: Range(attr.Range), 75 | NameRange: Range(attr.NameRange), 76 | } 77 | } 78 | 79 | blocks := make(hclext.Blocks, len(body.Blocks)) 80 | for idx, block := range body.Blocks { 81 | blockBody, contentDiags := BodyContent(block.Body) 82 | diags = diags.Extend(contentDiags) 83 | 84 | labelRanges := make([]hcl.Range, len(block.LabelRanges)) 85 | for idx, labelRange := range block.LabelRanges { 86 | labelRanges[idx] = Range(labelRange) 87 | } 88 | 89 | blocks[idx] = &hclext.Block{ 90 | Type: block.Type, 91 | Labels: block.Labels, 92 | Body: blockBody, 93 | DefRange: Range(block.DefRange), 94 | TypeRange: Range(block.TypeRange), 95 | LabelRanges: labelRanges, 96 | } 97 | } 98 | 99 | return &hclext.BodyContent{ 100 | Attributes: attributes, 101 | Blocks: blocks, 102 | }, diags 103 | } 104 | 105 | // RuleObject is an intermediate representation that satisfies the Rule interface. 106 | type RuleObject struct { 107 | tflint.DefaultRule 108 | Data struct { 109 | Name string 110 | Enabled bool 111 | Severity tflint.Severity 112 | Link string 113 | } 114 | } 115 | 116 | // Name returns the rule name 117 | func (r *RuleObject) Name() string { return r.Data.Name } 118 | 119 | // Enabled returns whether the rule is enabled 120 | func (r *RuleObject) Enabled() bool { return r.Data.Enabled } 121 | 122 | // Severity returns the severify of the rule 123 | func (r *RuleObject) Severity() tflint.Severity { return r.Data.Severity } 124 | 125 | // Link returns the link of the rule documentation if exists 126 | func (r *RuleObject) Link() string { return r.Data.Link } 127 | 128 | // Check does nothing. This is just a method to satisfy the interface 129 | func (r *RuleObject) Check(tflint.Runner) error { return nil } 130 | 131 | // Rule converts proto.EmitIssue_Rule to RuleObject 132 | func Rule(rule *proto.EmitIssue_Rule) *RuleObject { 133 | if rule == nil { 134 | return nil 135 | } 136 | 137 | return &RuleObject{ 138 | Data: struct { 139 | Name string 140 | Enabled bool 141 | Severity tflint.Severity 142 | Link string 143 | }{ 144 | Name: rule.Name, 145 | Enabled: rule.Enabled, 146 | Severity: Severity(rule.Severity), 147 | Link: rule.Link, 148 | }, 149 | } 150 | } 151 | 152 | // Expression converts proto.Expression to hcl.Expression 153 | func Expression(expr *proto.Expression) (hcl.Expression, hcl.Diagnostics) { 154 | parsed, diags := hclext.ParseExpression(expr.Bytes, expr.Range.Filename, Pos(expr.Range.Start)) 155 | if diags.HasErrors() { 156 | return nil, diags 157 | } 158 | if expr.Value != nil { 159 | val, err := Value(expr.Value, cty.DynamicPseudoType, expr.ValueMarks) 160 | if err != nil { 161 | panic(fmt.Errorf("cannot unmarshal the bound expr: %w", err)) 162 | } 163 | parsed = hclext.BindValue(val, parsed) 164 | } 165 | return parsed, diags 166 | } 167 | 168 | // Severity converts proto.EmitIssue_Severity to severity 169 | func Severity(severity proto.EmitIssue_Severity) tflint.Severity { 170 | switch severity { 171 | case proto.EmitIssue_SEVERITY_ERROR: 172 | return tflint.ERROR 173 | case proto.EmitIssue_SEVERITY_WARNING: 174 | return tflint.WARNING 175 | case proto.EmitIssue_SEVERITY_NOTICE: 176 | return tflint.NOTICE 177 | } 178 | 179 | return tflint.ERROR 180 | } 181 | 182 | // Range converts proto.Range to hcl.Range 183 | func Range(rng *proto.Range) hcl.Range { 184 | if rng == nil { 185 | return hcl.Range{} 186 | } 187 | 188 | return hcl.Range{ 189 | Filename: rng.Filename, 190 | Start: Pos(rng.Start), 191 | End: Pos(rng.End), 192 | } 193 | } 194 | 195 | // Pos converts proto.Range_Pos to hcl.Pos 196 | func Pos(pos *proto.Range_Pos) hcl.Pos { 197 | if pos == nil { 198 | return hcl.Pos{} 199 | } 200 | 201 | return hcl.Pos{ 202 | Line: int(pos.Line), 203 | Column: int(pos.Column), 204 | Byte: int(pos.Byte), 205 | } 206 | } 207 | 208 | // Value converts msgpack and []proto.ValueMark to cty.Value 209 | func Value(value []byte, ty cty.Type, valueMarks []*proto.ValueMark) (cty.Value, error) { 210 | val, err := msgpack.Unmarshal(value, ty) 211 | if err != nil { 212 | return cty.NilVal, err 213 | } 214 | 215 | pvm := make([]cty.PathValueMarks, len(valueMarks)) 216 | for idx, mark := range valueMarks { 217 | pvm[idx] = cty.PathValueMarks{ 218 | Path: AttributePath(mark.Path), 219 | } 220 | vm := []interface{}{} 221 | if mark.Sensitive { 222 | vm = append(vm, marks.Sensitive) 223 | } 224 | if mark.Ephemeral { 225 | vm = append(vm, marks.Ephemeral) 226 | } 227 | pvm[idx].Marks = cty.NewValueMarks(vm...) 228 | } 229 | 230 | return val.MarkWithPaths(pvm), nil 231 | } 232 | 233 | // AttributePath converts proto.AttributePath to cty.Path 234 | func AttributePath(path *proto.AttributePath) cty.Path { 235 | ret := cty.Path{} 236 | 237 | for _, step := range path.Steps { 238 | switch s := step.Selector.(type) { 239 | case *proto.AttributePath_Step_ElementKeyString: 240 | ret = ret.IndexString(s.ElementKeyString) 241 | case *proto.AttributePath_Step_ElementKeyInt: 242 | ret = ret.IndexInt(int(s.ElementKeyInt)) 243 | case *proto.AttributePath_Step_AttributeName: 244 | ret = ret.GetAttr(s.AttributeName) 245 | } 246 | } 247 | return ret 248 | } 249 | 250 | // Config converts proto.ApplyGlobalConfig_Config to tflint.Config 251 | func Config(config *proto.ApplyGlobalConfig_Config) *tflint.Config { 252 | if config == nil { 253 | return &tflint.Config{Rules: make(map[string]*tflint.RuleConfig)} 254 | } 255 | 256 | rules := map[string]*tflint.RuleConfig{} 257 | for name, rule := range config.Rules { 258 | rules[name] = &tflint.RuleConfig{Name: rule.Name, Enabled: rule.Enabled} 259 | } 260 | return &tflint.Config{ 261 | Rules: rules, 262 | DisabledByDefault: config.DisabledByDefault, 263 | Only: config.Only, 264 | Fix: config.Fix, 265 | } 266 | } 267 | 268 | // GetModuleContentOption converts proto.GetModuleContent_Option to tflint.GetModuleContentOption 269 | func GetModuleContentOption(opts *proto.GetModuleContent_Option) tflint.GetModuleContentOption { 270 | if opts == nil { 271 | return tflint.GetModuleContentOption{} 272 | } 273 | 274 | return tflint.GetModuleContentOption{ 275 | ModuleCtx: ModuleCtxType(opts.ModuleCtx), 276 | ExpandMode: ExpandMode(opts.ExpandMode), 277 | Hint: GetModuleContentHint(opts.Hint), 278 | } 279 | } 280 | 281 | // ModuleCtxType converts proto.ModuleCtxType to tflint.ModuleCtxType 282 | func ModuleCtxType(ty proto.ModuleCtxType) tflint.ModuleCtxType { 283 | switch ty { 284 | case proto.ModuleCtxType_MODULE_CTX_TYPE_UNSPECIFIED: 285 | return tflint.SelfModuleCtxType 286 | case proto.ModuleCtxType_MODULE_CTX_TYPE_SELF: 287 | return tflint.SelfModuleCtxType 288 | case proto.ModuleCtxType_MODULE_CTX_TYPE_ROOT: 289 | return tflint.RootModuleCtxType 290 | default: 291 | panic(fmt.Sprintf("invalid ModuleCtxType: %s", ty)) 292 | } 293 | } 294 | 295 | // ExpandMode converts proto.GetModuleContent_ExpandMode to tflint.ExpandMode 296 | func ExpandMode(mode proto.GetModuleContent_ExpandMode) tflint.ExpandMode { 297 | switch mode { 298 | case proto.GetModuleContent_EXPAND_MODE_UNSPECIFIED: 299 | return tflint.ExpandModeExpand 300 | case proto.GetModuleContent_EXPAND_MODE_EXPAND: 301 | return tflint.ExpandModeExpand 302 | case proto.GetModuleContent_EXPAND_MODE_NONE: 303 | return tflint.ExpandModeNone 304 | default: 305 | panic(fmt.Sprintf("invalid ExpandMode: %s", mode)) 306 | } 307 | } 308 | 309 | // GetModuleContentHint converts proto.GetModuleContent_Hint to tflint.GetModuleContentHint 310 | func GetModuleContentHint(hint *proto.GetModuleContent_Hint) tflint.GetModuleContentHint { 311 | if hint == nil { 312 | return tflint.GetModuleContentHint{} 313 | } 314 | 315 | return tflint.GetModuleContentHint{ 316 | ResourceType: hint.ResourceType, 317 | } 318 | } 319 | 320 | // Error converts gRPC error status to wrapped error 321 | func Error(err error) error { 322 | if err == nil { 323 | return nil 324 | } 325 | 326 | st, ok := status.FromError(err) 327 | if !ok { 328 | return err 329 | } 330 | 331 | // Unimplemented is an unexpected error, so return as-is. 332 | if st.Code() == codes.Unimplemented { 333 | return err 334 | } 335 | 336 | // If the error status has no details, return an error from the gRPC error status. 337 | // Remove the status code because some statuses are expected and should not be shown to users. 338 | if len(st.Details()) == 0 { 339 | return errors.New(st.Message()) 340 | } 341 | 342 | // It is not supposed to have multiple details. The detail have an error code and will be wrapped as an error. 343 | switch t := st.Details()[0].(type) { 344 | case *proto.ErrorDetail: 345 | switch t.Code { 346 | case proto.ErrorCode_ERROR_CODE_SENSITIVE: 347 | return tflint.ErrSensitive 348 | } 349 | } 350 | 351 | return err 352 | } 353 | -------------------------------------------------------------------------------- /terraform/addrs/parse_ref.go: -------------------------------------------------------------------------------- 1 | package addrs 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | ) 8 | 9 | // Reference describes a reference to an address with source location 10 | // information. 11 | type Reference struct { 12 | Subject Referenceable 13 | SourceRange hcl.Range 14 | Remaining hcl.Traversal 15 | } 16 | 17 | // ParseRef attempts to extract a referencable address from the prefix of the 18 | // given traversal, which must be an absolute traversal or this function 19 | // will panic. 20 | // 21 | // If no error diagnostics are returned, the returned reference includes the 22 | // address that was extracted, the source range it was extracted from, and any 23 | // remaining relative traversal that was not consumed as part of the 24 | // reference. 25 | // 26 | // If error diagnostics are returned then the Reference value is invalid and 27 | // must not be used. 28 | func ParseRef(traversal hcl.Traversal) (*Reference, hcl.Diagnostics) { 29 | ref, diags := parseRef(traversal) 30 | 31 | // Normalize a little to make life easier for callers. 32 | if ref != nil { 33 | if len(ref.Remaining) == 0 { 34 | ref.Remaining = nil 35 | } 36 | } 37 | 38 | return ref, diags 39 | } 40 | 41 | func parseRef(traversal hcl.Traversal) (*Reference, hcl.Diagnostics) { 42 | var diags hcl.Diagnostics 43 | 44 | root := traversal.RootName() 45 | rootRange := traversal[0].SourceRange() 46 | 47 | switch root { 48 | 49 | case "count": 50 | name, rng, remain, diags := parseSingleAttrRef(traversal) 51 | return &Reference{ 52 | Subject: CountAttr{Name: name}, 53 | SourceRange: rng, 54 | Remaining: remain, 55 | }, diags 56 | 57 | case "each": 58 | name, rng, remain, diags := parseSingleAttrRef(traversal) 59 | return &Reference{ 60 | Subject: ForEachAttr{Name: name}, 61 | SourceRange: rng, 62 | Remaining: remain, 63 | }, diags 64 | 65 | case "data": 66 | if len(traversal) < 3 { 67 | diags = diags.Append(&hcl.Diagnostic{ 68 | Severity: hcl.DiagError, 69 | Summary: "Invalid reference", 70 | Detail: `The "data" object must be followed by two attribute names: the data source type and the resource name.`, 71 | Subject: traversal.SourceRange().Ptr(), 72 | }) 73 | return nil, diags 74 | } 75 | remain := traversal[1:] // trim off "data" so we can use our shared resource reference parser 76 | return parseResourceRef(DataResourceMode, rootRange, remain) 77 | 78 | case "resource": 79 | // This is an alias for the normal case of just using a managed resource 80 | // type as a top-level symbol, which will serve as an escape mechanism 81 | // if a later edition of the Terraform language introduces a new 82 | // reference prefix that conflicts with a resource type name in an 83 | // existing provider. In that case, the edition upgrade tool can 84 | // rewrite foo.bar into resource.foo.bar to ensure that "foo" remains 85 | // interpreted as a resource type name rather than as the new reserved 86 | // word. 87 | if len(traversal) < 3 { 88 | diags = diags.Append(&hcl.Diagnostic{ 89 | Severity: hcl.DiagError, 90 | Summary: "Invalid reference", 91 | Detail: `The "resource" object must be followed by two attribute names: the resource type and the resource name.`, 92 | Subject: traversal.SourceRange().Ptr(), 93 | }) 94 | return nil, diags 95 | } 96 | remain := traversal[1:] // trim off "resource" so we can use our shared resource reference parser 97 | return parseResourceRef(ManagedResourceMode, rootRange, remain) 98 | 99 | case "local": 100 | name, rng, remain, diags := parseSingleAttrRef(traversal) 101 | return &Reference{ 102 | Subject: LocalValue{Name: name}, 103 | SourceRange: rng, 104 | Remaining: remain, 105 | }, diags 106 | 107 | case "module": 108 | callName, callRange, remain, diags := parseSingleAttrRef(traversal) 109 | if diags.HasErrors() { 110 | return nil, diags 111 | } 112 | 113 | // A traversal starting with "module" can either be a reference to an 114 | // entire module, or to a single output from a module instance, 115 | // depending on what we find after this introducer. 116 | callInstance := ModuleCallInstance{ 117 | Call: ModuleCall{ 118 | Name: callName, 119 | }, 120 | Key: NoKey, 121 | } 122 | 123 | if len(remain) == 0 { 124 | // Reference to an entire module. Might alternatively be a 125 | // reference to a single instance of a particular module, but the 126 | // caller will need to deal with that ambiguity since we don't have 127 | // enough context here. 128 | return &Reference{ 129 | Subject: callInstance.Call, 130 | SourceRange: callRange, 131 | Remaining: remain, 132 | }, diags 133 | } 134 | 135 | if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { 136 | var err error 137 | callInstance.Key, err = ParseInstanceKey(idxTrav.Key) 138 | if err != nil { 139 | diags = diags.Append(&hcl.Diagnostic{ 140 | Severity: hcl.DiagError, 141 | Summary: "Invalid index key", 142 | Detail: fmt.Sprintf("Invalid index for module instance: %s.", err), 143 | Subject: &idxTrav.SrcRange, 144 | }) 145 | return nil, diags 146 | } 147 | remain = remain[1:] 148 | 149 | if len(remain) == 0 { 150 | // Also a reference to an entire module instance, but we have a key 151 | // now. 152 | return &Reference{ 153 | Subject: callInstance, 154 | SourceRange: hcl.RangeBetween(callRange, idxTrav.SrcRange), 155 | Remaining: remain, 156 | }, diags 157 | } 158 | } 159 | 160 | if attrTrav, ok := remain[0].(hcl.TraverseAttr); ok { 161 | remain = remain[1:] 162 | return &Reference{ 163 | Subject: ModuleCallInstanceOutput{ 164 | Name: attrTrav.Name, 165 | Call: callInstance, 166 | }, 167 | SourceRange: hcl.RangeBetween(callRange, attrTrav.SrcRange), 168 | Remaining: remain, 169 | }, diags 170 | } 171 | 172 | diags = diags.Append(&hcl.Diagnostic{ 173 | Severity: hcl.DiagError, 174 | Summary: "Invalid reference", 175 | Detail: "Module instance objects do not support this operation.", 176 | Subject: remain[0].SourceRange().Ptr(), 177 | }) 178 | return nil, diags 179 | 180 | case "path": 181 | name, rng, remain, diags := parseSingleAttrRef(traversal) 182 | return &Reference{ 183 | Subject: PathAttr{Name: name}, 184 | SourceRange: rng, 185 | Remaining: remain, 186 | }, diags 187 | 188 | case "self": 189 | return &Reference{ 190 | Subject: Self, 191 | SourceRange: rootRange, 192 | Remaining: traversal[1:], 193 | }, diags 194 | 195 | case "terraform": 196 | name, rng, remain, diags := parseSingleAttrRef(traversal) 197 | return &Reference{ 198 | Subject: TerraformAttr{Name: name}, 199 | SourceRange: rng, 200 | Remaining: remain, 201 | }, diags 202 | 203 | case "var": 204 | name, rng, remain, diags := parseSingleAttrRef(traversal) 205 | return &Reference{ 206 | Subject: InputVariable{Name: name}, 207 | SourceRange: rng, 208 | Remaining: remain, 209 | }, diags 210 | 211 | case "template", "lazy", "arg": 212 | // These names are all pre-emptively reserved in the hope of landing 213 | // some version of "template values" or "lazy expressions" feature 214 | // before the next opt-in language edition, but don't yet do anything. 215 | diags = diags.Append(&hcl.Diagnostic{ 216 | Severity: hcl.DiagError, 217 | Summary: "Reserved symbol name", 218 | Detail: fmt.Sprintf("The symbol name %q is reserved for use in a future Terraform version. If you are using a provider that already uses this as a resource type name, add the prefix \"resource.\" to force interpretation as a resource type name.", root), 219 | Subject: rootRange.Ptr(), 220 | }) 221 | return nil, diags 222 | 223 | default: 224 | return parseResourceRef(ManagedResourceMode, rootRange, traversal) 225 | } 226 | } 227 | 228 | func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Traversal) (*Reference, hcl.Diagnostics) { 229 | var diags hcl.Diagnostics 230 | 231 | if len(traversal) < 2 { 232 | diags = diags.Append(&hcl.Diagnostic{ 233 | Severity: hcl.DiagError, 234 | Summary: "Invalid reference", 235 | Detail: `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`, 236 | Subject: hcl.RangeBetween(traversal[0].SourceRange(), traversal[len(traversal)-1].SourceRange()).Ptr(), 237 | }) 238 | return nil, diags 239 | } 240 | 241 | var typeName, name string 242 | switch tt := traversal[0].(type) { // Could be either root or attr, depending on our resource mode 243 | case hcl.TraverseRoot: 244 | typeName = tt.Name 245 | case hcl.TraverseAttr: 246 | typeName = tt.Name 247 | default: 248 | // If it isn't a TraverseRoot then it must be a "data" reference. 249 | diags = diags.Append(&hcl.Diagnostic{ 250 | Severity: hcl.DiagError, 251 | Summary: "Invalid reference", 252 | Detail: `The "data" object does not support this operation.`, 253 | Subject: traversal[0].SourceRange().Ptr(), 254 | }) 255 | return nil, diags 256 | } 257 | 258 | attrTrav, ok := traversal[1].(hcl.TraverseAttr) 259 | if !ok { 260 | var what string 261 | switch mode { 262 | case DataResourceMode: 263 | what = "data source" 264 | default: 265 | what = "resource type" 266 | } 267 | diags = diags.Append(&hcl.Diagnostic{ 268 | Severity: hcl.DiagError, 269 | Summary: "Invalid reference", 270 | Detail: fmt.Sprintf(`A reference to a %s must be followed by at least one attribute access, specifying the resource name.`, what), 271 | Subject: traversal[1].SourceRange().Ptr(), 272 | }) 273 | return nil, diags 274 | } 275 | name = attrTrav.Name 276 | rng := hcl.RangeBetween(startRange, attrTrav.SrcRange) 277 | remain := traversal[2:] 278 | 279 | resourceAddr := Resource{ 280 | Mode: mode, 281 | Type: typeName, 282 | Name: name, 283 | } 284 | resourceInstAddr := ResourceInstance{ 285 | Resource: resourceAddr, 286 | Key: NoKey, 287 | } 288 | 289 | if len(remain) == 0 { 290 | // This might actually be a reference to the collection of all instances 291 | // of the resource, but we don't have enough context here to decide 292 | // so we'll let the caller resolve that ambiguity. 293 | return &Reference{ 294 | Subject: resourceAddr, 295 | SourceRange: rng, 296 | }, diags 297 | } 298 | 299 | if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { 300 | var err error 301 | resourceInstAddr.Key, err = ParseInstanceKey(idxTrav.Key) 302 | if err != nil { 303 | diags = diags.Append(&hcl.Diagnostic{ 304 | Severity: hcl.DiagError, 305 | Summary: "Invalid index key", 306 | Detail: fmt.Sprintf("Invalid index for resource instance: %s.", err), 307 | Subject: &idxTrav.SrcRange, 308 | }) 309 | return nil, diags 310 | } 311 | remain = remain[1:] 312 | rng = hcl.RangeBetween(rng, idxTrav.SrcRange) 313 | } 314 | 315 | return &Reference{ 316 | Subject: resourceInstAddr, 317 | SourceRange: rng, 318 | Remaining: remain, 319 | }, diags 320 | } 321 | 322 | func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, hcl.Diagnostics) { 323 | var diags hcl.Diagnostics 324 | 325 | root := traversal.RootName() 326 | rootRange := traversal[0].SourceRange() 327 | 328 | if len(traversal) < 2 { 329 | diags = diags.Append(&hcl.Diagnostic{ 330 | Severity: hcl.DiagError, 331 | Summary: "Invalid reference", 332 | Detail: fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root), 333 | Subject: &rootRange, 334 | }) 335 | return "", hcl.Range{}, nil, diags 336 | } 337 | if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok { 338 | return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags 339 | } 340 | diags = diags.Append(&hcl.Diagnostic{ 341 | Severity: hcl.DiagError, 342 | Summary: "Invalid reference", 343 | Detail: fmt.Sprintf("The %q object does not support this operation.", root), 344 | Subject: traversal[1].SourceRange().Ptr(), 345 | }) 346 | return "", hcl.Range{}, nil, diags 347 | } 348 | -------------------------------------------------------------------------------- /helper/runner.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | 9 | "github.com/hashicorp/hcl/v2" 10 | "github.com/hashicorp/hcl/v2/gohcl" 11 | "github.com/hashicorp/hcl/v2/hclsyntax" 12 | "github.com/terraform-linters/tflint-plugin-sdk/hclext" 13 | "github.com/terraform-linters/tflint-plugin-sdk/internal" 14 | "github.com/terraform-linters/tflint-plugin-sdk/terraform/addrs" 15 | "github.com/terraform-linters/tflint-plugin-sdk/terraform/lang/marks" 16 | "github.com/terraform-linters/tflint-plugin-sdk/tflint" 17 | "github.com/zclconf/go-cty/cty" 18 | "github.com/zclconf/go-cty/cty/convert" 19 | "github.com/zclconf/go-cty/cty/gocty" 20 | ) 21 | 22 | // Runner is a mock that satisfies the Runner interface for plugin testing. 23 | type Runner struct { 24 | Issues Issues 25 | 26 | files map[string]*hcl.File 27 | sources map[string][]byte 28 | config Config 29 | variables map[string]*Variable 30 | fixer *internal.Fixer 31 | } 32 | 33 | // Variable is an implementation of variables in Terraform language 34 | type Variable struct { 35 | Name string 36 | Default cty.Value 37 | DeclRange hcl.Range 38 | } 39 | 40 | // Config is a pseudo TFLint config file object for testing from plugins. 41 | type Config struct { 42 | Rules []RuleConfig `hcl:"rule,block"` 43 | } 44 | 45 | // RuleConfig is a pseudo TFLint config file object for testing from plugins. 46 | type RuleConfig struct { 47 | Name string `hcl:"name,label"` 48 | Enabled bool `hcl:"enabled"` 49 | Body hcl.Body `hcl:",remain"` 50 | } 51 | 52 | var _ tflint.Runner = &Runner{} 53 | 54 | // GetOriginalwd always returns the current directory 55 | func (r *Runner) GetOriginalwd() (string, error) { 56 | return os.Getwd() 57 | } 58 | 59 | // GetModulePath always returns the root module path address 60 | func (r *Runner) GetModulePath() (addrs.Module, error) { 61 | return []string{}, nil 62 | } 63 | 64 | // GetModuleContent gets a content of the current module 65 | func (r *Runner) GetModuleContent(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) { 66 | content := &hclext.BodyContent{} 67 | diags := hcl.Diagnostics{} 68 | 69 | for _, f := range r.files { 70 | c, d := hclext.PartialContent(f.Body, schema) 71 | diags = diags.Extend(d) 72 | for name, attr := range c.Attributes { 73 | content.Attributes[name] = attr 74 | } 75 | content.Blocks = append(content.Blocks, c.Blocks...) 76 | } 77 | 78 | if diags.HasErrors() { 79 | return nil, diags 80 | } 81 | return content, nil 82 | } 83 | 84 | // GetResourceContent gets a resource content of the current module 85 | func (r *Runner) GetResourceContent(name string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) { 86 | body, err := r.GetModuleContent(&hclext.BodySchema{ 87 | Blocks: []hclext.BlockSchema{ 88 | {Type: "resource", LabelNames: []string{"type", "name"}, Body: schema}, 89 | }, 90 | }, opts) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | content := &hclext.BodyContent{Blocks: []*hclext.Block{}} 96 | for _, resource := range body.Blocks { 97 | if resource.Labels[0] != name { 98 | continue 99 | } 100 | 101 | content.Blocks = append(content.Blocks, resource) 102 | } 103 | 104 | return content, nil 105 | } 106 | 107 | // GetProviderContent gets a provider content of the current module 108 | func (r *Runner) GetProviderContent(name string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) { 109 | body, err := r.GetModuleContent(&hclext.BodySchema{ 110 | Blocks: []hclext.BlockSchema{ 111 | {Type: "provider", LabelNames: []string{"name"}, Body: schema}, 112 | }, 113 | }, opts) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | content := &hclext.BodyContent{Blocks: []*hclext.Block{}} 119 | for _, provider := range body.Blocks { 120 | if provider.Labels[0] != name { 121 | continue 122 | } 123 | 124 | content.Blocks = append(content.Blocks, provider) 125 | } 126 | 127 | return content, nil 128 | } 129 | 130 | // GetFile returns the hcl.File object 131 | func (r *Runner) GetFile(filename string) (*hcl.File, error) { 132 | return r.files[filename], nil 133 | } 134 | 135 | // GetFiles returns all hcl.File 136 | func (r *Runner) GetFiles() (map[string]*hcl.File, error) { 137 | return r.files, nil 138 | } 139 | 140 | type nativeWalker struct { 141 | walker tflint.ExprWalker 142 | } 143 | 144 | func (w *nativeWalker) Enter(node hclsyntax.Node) hcl.Diagnostics { 145 | if expr, ok := node.(hcl.Expression); ok { 146 | return w.walker.Enter(expr) 147 | } 148 | return nil 149 | } 150 | 151 | func (w *nativeWalker) Exit(node hclsyntax.Node) hcl.Diagnostics { 152 | if expr, ok := node.(hcl.Expression); ok { 153 | return w.walker.Exit(expr) 154 | } 155 | return nil 156 | } 157 | 158 | // WalkExpressions traverses expressions in all files by the passed walker. 159 | func (r *Runner) WalkExpressions(walker tflint.ExprWalker) hcl.Diagnostics { 160 | diags := hcl.Diagnostics{} 161 | for _, file := range r.files { 162 | if body, ok := file.Body.(*hclsyntax.Body); ok { 163 | walkDiags := hclsyntax.Walk(body, &nativeWalker{walker: walker}) 164 | diags = diags.Extend(walkDiags) 165 | continue 166 | } 167 | 168 | // In JSON syntax, everything can be walked as an attribute. 169 | attrs, jsonDiags := file.Body.JustAttributes() 170 | if jsonDiags.HasErrors() { 171 | diags = diags.Extend(jsonDiags) 172 | continue 173 | } 174 | 175 | for _, attr := range attrs { 176 | enterDiags := walker.Enter(attr.Expr) 177 | diags = diags.Extend(enterDiags) 178 | exitDiags := walker.Exit(attr.Expr) 179 | diags = diags.Extend(exitDiags) 180 | } 181 | } 182 | 183 | return diags 184 | } 185 | 186 | // DecodeRuleConfig extracts the rule's configuration into the given value 187 | func (r *Runner) DecodeRuleConfig(name string, ret interface{}) error { 188 | schema := hclext.ImpliedBodySchema(ret) 189 | 190 | for _, rule := range r.config.Rules { 191 | if rule.Name == name { 192 | body, diags := hclext.Content(rule.Body, schema) 193 | if diags.HasErrors() { 194 | return diags 195 | } 196 | if diags := hclext.DecodeBody(body, nil, ret); diags.HasErrors() { 197 | return diags 198 | } 199 | return nil 200 | } 201 | } 202 | 203 | return nil 204 | } 205 | 206 | var errRefTy = reflect.TypeOf((*error)(nil)).Elem() 207 | 208 | // EvaluateExpr returns a value of the passed expression. 209 | // Note that some features are limited 210 | func (r *Runner) EvaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error { 211 | rval := reflect.ValueOf(target) 212 | rty := rval.Type() 213 | 214 | var callback bool 215 | switch rty.Kind() { 216 | case reflect.Func: 217 | // Callback must meet the following requirements: 218 | // - It must be a function 219 | // - It must take an argument 220 | // - It must return an error 221 | if !(rty.NumIn() == 1 && rty.NumOut() == 1 && rty.Out(0).Implements(errRefTy)) { 222 | panic(`callback must be of type "func (v T) error"`) 223 | } 224 | callback = true 225 | target = reflect.New(rty.In(0)).Interface() 226 | 227 | case reflect.Pointer: 228 | // ok 229 | default: 230 | panic("target value is not a pointer or function") 231 | } 232 | 233 | err := r.evaluateExpr(expr, target, opts) 234 | if !callback { 235 | // error should be handled in the caller 236 | return err 237 | } 238 | 239 | if err != nil { 240 | // If it cannot be represented as a Go value, exit without invoking the callback rather than returning an error. 241 | if errors.Is(err, tflint.ErrUnknownValue) || errors.Is(err, tflint.ErrNullValue) || errors.Is(err, tflint.ErrSensitive) || errors.Is(err, tflint.ErrUnevaluable) { 242 | return nil 243 | } 244 | return err 245 | } 246 | 247 | rerr := rval.Call([]reflect.Value{reflect.ValueOf(target).Elem()}) 248 | if rerr[0].IsNil() { 249 | return nil 250 | } 251 | return rerr[0].Interface().(error) 252 | } 253 | 254 | func (r *Runner) evaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error { 255 | if opts == nil { 256 | opts = &tflint.EvaluateExprOption{} 257 | } 258 | 259 | var ty cty.Type 260 | if opts.WantType != nil { 261 | ty = *opts.WantType 262 | } else { 263 | switch target.(type) { 264 | case *string: 265 | ty = cty.String 266 | case *int: 267 | ty = cty.Number 268 | case *bool: 269 | ty = cty.Bool 270 | case *[]string: 271 | ty = cty.List(cty.String) 272 | case *[]int: 273 | ty = cty.List(cty.Number) 274 | case *[]bool: 275 | ty = cty.List(cty.Bool) 276 | case *map[string]string: 277 | ty = cty.Map(cty.String) 278 | case *map[string]int: 279 | ty = cty.Map(cty.Number) 280 | case *map[string]bool: 281 | ty = cty.Map(cty.Bool) 282 | case *cty.Value: 283 | ty = cty.DynamicPseudoType 284 | default: 285 | return fmt.Errorf("unsupported target type: %T", target) 286 | } 287 | } 288 | 289 | variables := map[string]cty.Value{} 290 | for _, variable := range r.variables { 291 | variables[variable.Name] = variable.Default 292 | } 293 | workspace, success := os.LookupEnv("TF_WORKSPACE") 294 | if !success { 295 | workspace = "default" 296 | } 297 | rawVal, diags := expr.Value(&hcl.EvalContext{ 298 | Variables: map[string]cty.Value{ 299 | "var": cty.ObjectVal(variables), 300 | "terraform": cty.ObjectVal(map[string]cty.Value{ 301 | "workspace": cty.StringVal(workspace), 302 | }), 303 | }, 304 | }) 305 | if diags.HasErrors() { 306 | return diags 307 | } 308 | val, err := convert.Convert(rawVal, ty) 309 | if err != nil { 310 | return err 311 | } 312 | 313 | return gocty.FromCtyValue(val, target) 314 | } 315 | 316 | // EmitIssue adds an issue to the runner itself. 317 | func (r *Runner) EmitIssue(rule tflint.Rule, message string, location hcl.Range) error { 318 | r.Issues = append(r.Issues, &Issue{ 319 | Rule: rule, 320 | Message: message, 321 | Range: location, 322 | }) 323 | return nil 324 | } 325 | 326 | // EmitIssueWithFix adds an issue and invoke fix. 327 | func (r *Runner) EmitIssueWithFix(rule tflint.Rule, message string, location hcl.Range, fixFunc func(f tflint.Fixer) error) error { 328 | r.fixer.StashChanges() 329 | if err := fixFunc(r.fixer); err != nil { 330 | if errors.Is(err, tflint.ErrFixNotSupported) { 331 | r.fixer.PopChangesFromStash() 332 | return r.EmitIssue(rule, message, location) 333 | } 334 | return err 335 | } 336 | return r.EmitIssue(rule, message, location) 337 | } 338 | 339 | // Changes returns formatted changes by the fixer. 340 | func (r *Runner) Changes() map[string][]byte { 341 | r.fixer.FormatChanges() 342 | return r.fixer.Changes() 343 | } 344 | 345 | // EnsureNoError is a method that simply runs a function if there is no error. 346 | // 347 | // Deprecated: Use EvaluateExpr with a function callback. e.g. EvaluateExpr(expr, func (val T) error {}, ...) 348 | func (r *Runner) EnsureNoError(err error, proc func() error) error { 349 | if err == nil { 350 | return proc() 351 | } 352 | return err 353 | } 354 | 355 | // newLocalRunner initialises a new test runner. 356 | func newLocalRunner(files map[string]*hcl.File, issues Issues) *Runner { 357 | return &Runner{ 358 | files: map[string]*hcl.File{}, 359 | sources: map[string][]byte{}, 360 | variables: map[string]*Variable{}, 361 | Issues: issues, 362 | } 363 | } 364 | 365 | // addLocalFile adds a new file to the current mapped files. 366 | // For testing only. Normally, the main TFLint process is responsible for loading files. 367 | func (r *Runner) addLocalFile(name string, file *hcl.File) bool { 368 | if _, exists := r.files[name]; exists { 369 | return false 370 | } 371 | 372 | r.files[name] = file 373 | r.sources[name] = file.Bytes 374 | return true 375 | } 376 | 377 | // initFromFiles initializes the runner from locally added files. 378 | // For testing only. 379 | func (r *Runner) initFromFiles() error { 380 | for _, file := range r.files { 381 | content, _, diags := file.Body.PartialContent(configFileSchema) 382 | if diags.HasErrors() { 383 | return diags 384 | } 385 | 386 | for _, block := range content.Blocks { 387 | switch block.Type { 388 | case "variable": 389 | variable, diags := decodeVariableBlock(block) 390 | if diags.HasErrors() { 391 | return diags 392 | } 393 | r.variables[variable.Name] = variable 394 | default: 395 | continue 396 | } 397 | } 398 | } 399 | r.fixer = internal.NewFixer(r.sources) 400 | 401 | return nil 402 | } 403 | 404 | func decodeVariableBlock(block *hcl.Block) (*Variable, hcl.Diagnostics) { 405 | v := &Variable{ 406 | Name: block.Labels[0], 407 | DeclRange: block.DefRange, 408 | } 409 | 410 | content, _, diags := block.Body.PartialContent(&hcl.BodySchema{ 411 | Attributes: []hcl.AttributeSchema{ 412 | { 413 | Name: "default", 414 | }, 415 | { 416 | Name: "sensitive", 417 | }, 418 | { 419 | Name: "ephemeral", 420 | }, 421 | }, 422 | }) 423 | if diags.HasErrors() { 424 | return v, diags 425 | } 426 | 427 | if attr, exists := content.Attributes["default"]; exists { 428 | val, diags := attr.Expr.Value(nil) 429 | if diags.HasErrors() { 430 | return v, diags 431 | } 432 | 433 | v.Default = val 434 | } 435 | if attr, exists := content.Attributes["sensitive"]; exists { 436 | var sensitive bool 437 | diags := gohcl.DecodeExpression(attr.Expr, nil, &sensitive) 438 | if diags.HasErrors() { 439 | return v, diags 440 | } 441 | 442 | v.Default = v.Default.Mark(marks.Sensitive) 443 | } 444 | if attr, exists := content.Attributes["ephemeral"]; exists { 445 | var ephemeral bool 446 | diags := gohcl.DecodeExpression(attr.Expr, nil, &ephemeral) 447 | if diags.HasErrors() { 448 | return v, diags 449 | } 450 | 451 | v.Default = v.Default.Mark(marks.Ephemeral) 452 | } 453 | 454 | return v, nil 455 | } 456 | 457 | var configFileSchema = &hcl.BodySchema{ 458 | Blocks: []hcl.BlockHeaderSchema{ 459 | { 460 | Type: "variable", 461 | LabelNames: []string{"name"}, 462 | }, 463 | }, 464 | } 465 | -------------------------------------------------------------------------------- /tflint/interface.go: -------------------------------------------------------------------------------- 1 | package tflint 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/terraform-linters/tflint-plugin-sdk/hclext" 6 | "github.com/terraform-linters/tflint-plugin-sdk/terraform/addrs" 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | // RuleSet is a list of rules that a plugin should provide. 11 | // Normally, plugins can use BuiltinRuleSet directly, 12 | // but you can also use custom rulesets that satisfy this interface. 13 | // The actual implementation can be found in plugin/host2plugin.GRPCServer. 14 | type RuleSet interface { 15 | // RuleSetName is the name of the ruleset. This method is not expected to be overridden. 16 | RuleSetName() string 17 | 18 | // RuleSetVersion is the version of the plugin. This method is not expected to be overridden. 19 | RuleSetVersion() string 20 | 21 | // RuleNames is a list of rule names provided by the plugin. This method is not expected to be overridden. 22 | RuleNames() []string 23 | 24 | // VersionConstraint declares the version of TFLint the plugin will work with. Default is no constraint. 25 | VersionConstraint() string 26 | 27 | // ConfigSchema returns the ruleset plugin config schema. 28 | // If you return a schema, TFLint will extract the config from .tflint.hcl based on that schema 29 | // and pass it to ApplyConfig. This schema should be a schema inside of "plugin" block. 30 | // If you don't need a config that controls the entire plugin, you don't need to override this method. 31 | // 32 | // It is recommended to use hclext.ImpliedBodySchema to generate the schema from the structure: 33 | // 34 | // ``` 35 | // type myPluginConfig struct { 36 | // Style string `hclext:"style"` 37 | // Description string `hclext:"description,optional"` 38 | // Detail Detail `hclext:"detail,block"` 39 | // } 40 | // 41 | // config := &myPluginConfig{} 42 | // hclext.ImpliedBodySchema(config) 43 | // ``` 44 | ConfigSchema() *hclext.BodySchema 45 | 46 | // ApplyGlobalConfig applies the common config to the ruleset. 47 | // This is not supposed to be overridden from custom rulesets. 48 | // Override the ApplyConfig if you want to apply the plugin's custom configuration. 49 | ApplyGlobalConfig(*Config) error 50 | 51 | // ApplyConfig applies the configuration to the ruleset. 52 | // Custom rulesets can override this method to reflect the plugin's custom configuration. 53 | // 54 | // You can reflect the body in the structure by using hclext.DecodeBody: 55 | // 56 | // ``` 57 | // type myPluginConfig struct { 58 | // Style string `hclext:"style"` 59 | // Description string `hclext:"description,optional"` 60 | // Detail Detail `hclext:"detail,block"` 61 | // } 62 | // 63 | // config := &myPluginConfig{} 64 | // hclext.DecodeBody(body, nil, config) 65 | // ``` 66 | ApplyConfig(*hclext.BodyContent) error 67 | 68 | // NewRunner returns a new runner based on the original runner. 69 | // Custom rulesets can override this method to inject a custom runner. 70 | NewRunner(Runner) (Runner, error) 71 | 72 | // BuiltinImpl returns the receiver itself as BuiltinRuleSet. 73 | // This is not supposed to be overridden from custom rulesets. 74 | BuiltinImpl() *BuiltinRuleSet 75 | 76 | // All Ruleset must embed the builtin ruleset. 77 | mustEmbedBuiltinRuleSet() 78 | } 79 | 80 | // Runner acts as a client for each plugin to query the host process about the Terraform configurations. 81 | // The actual implementation can be found in plugin/plugin2host.GRPCClient. 82 | type Runner interface { 83 | // GetOriginalwd returns the original working directory. 84 | // Normally this is equal to os.Getwd(), but differs if --chdir or --recursive is used. 85 | // If you need the absolute path of the file, joining with the original working directory is appropriate. 86 | GetOriginalwd() (string, error) 87 | 88 | // GetModulePath returns the current module path address. 89 | GetModulePath() (addrs.Module, error) 90 | 91 | // GetResourceContent retrieves the content of resources based on the passed schema. 92 | // The schema allows you to specify attributes and blocks that describe the structure needed for the inspection: 93 | // 94 | // ``` 95 | // runner.GetResourceContent("aws_instance", &hclext.BodySchema{ 96 | // Attributes: []hclext.AttributeSchema{{Name: "instance_type"}}, 97 | // Blocks: []hclext.BlockSchema{ 98 | // { 99 | // Type: "ebs_block_device", 100 | // Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: "volume_size"}}}, 101 | // }, 102 | // }, 103 | // }, nil) 104 | // ``` 105 | GetResourceContent(resourceName string, schema *hclext.BodySchema, option *GetModuleContentOption) (*hclext.BodyContent, error) 106 | 107 | // GetProviderContent retrieves the content of providers based on the passed schema. 108 | // This method is GetResourceContent for providers. 109 | GetProviderContent(providerName string, schema *hclext.BodySchema, option *GetModuleContentOption) (*hclext.BodyContent, error) 110 | 111 | // GetModuleContent retrieves the content of the module based on the passed schema. 112 | // GetResourceContent/GetProviderContent are syntactic sugar for GetModuleContent, which you can use to access other structures. 113 | GetModuleContent(schema *hclext.BodySchema, option *GetModuleContentOption) (*hclext.BodyContent, error) 114 | 115 | // GetFile returns the hcl.File object. 116 | // This is low level API for accessing information such as comments and syntax. 117 | // When accessing resources, expressions, etc, it is recommended to use high-level APIs. 118 | GetFile(filename string) (*hcl.File, error) 119 | 120 | // GetFiles returns a map[string]hcl.File object, where the key is the file name. 121 | // This is low level API for accessing information such as comments and syntax. 122 | GetFiles() (map[string]*hcl.File, error) 123 | 124 | // WalkExpressions traverses expressions in all files by the passed walker. 125 | // The walker can be passed any structure that satisfies the `tflint.ExprWalker` 126 | // interface, or a `tflint.ExprWalkFunc`. Example of passing function: 127 | // 128 | // ``` 129 | // runner.WalkExpressions(tflint.ExprWalkFunc(func (expr hcl.Expression) hcl.Diagnostics { 130 | // // Write code here 131 | // })) 132 | // ``` 133 | // 134 | // If you pass ExprWalkFunc, the function will be called for every expression. 135 | // Note that it behaves differently in native HCL syntax and JSON syntax. 136 | // 137 | // In the HCL syntax, `var.foo` and `var.bar` in `[var.foo, var.bar]` are 138 | // also passed to the walker. In other words, it traverses expressions recursively. 139 | // To avoid redundant checks, the walker should check the kind of expression. 140 | // 141 | // In the JSON syntax, only an expression of an attribute seen from the top 142 | // level of the file is passed. In other words, it doesn't traverse expressions 143 | // recursively. This is a limitation of JSON syntax. 144 | WalkExpressions(walker ExprWalker) hcl.Diagnostics 145 | 146 | // DecodeRuleConfig fetches the rule's configuration and reflects the result in the 2nd argument. 147 | // The argument is expected to be a pointer to a structure tagged with hclext: 148 | // 149 | // ``` 150 | // type myRuleConfig struct { 151 | // Style string `hclext:"style"` 152 | // Description string `hclext:"description,optional"` 153 | // Detail Detail `hclext:"detail,block"` 154 | // } 155 | // 156 | // config := &myRuleConfig{} 157 | // runner.DecodeRuleConfig("my_rule", config) 158 | // ``` 159 | // 160 | // See the hclext.DecodeBody documentation and examples for more details. 161 | DecodeRuleConfig(ruleName string, ret interface{}) error 162 | 163 | // EvaluateExpr evaluates an expression and assigns its value to a Go value target, 164 | // which must be a pointer or a function. Any other type of target will trigger a panic. 165 | // 166 | // For pointers, if the expression value cannot be assigned to the target, an error is returned. 167 | // Some examples of this include unknown values (like variables without defaults or 168 | // aws_instance.foo.arn), null values, and sensitive values (for variables with sensitive = true). 169 | // 170 | // These errors be handled with errors.Is(): 171 | // 172 | // ``` 173 | // var val string 174 | // err := runner.EvaluateExpr(expr, &val, nil) 175 | // if err != nil { 176 | // if errors.Is(err, tflint.ErrUnknownValue) { 177 | // // Ignore unknown values 178 | // return nil 179 | // } 180 | // if errors.Is(err, tflint.ErrNullValue) { 181 | // // Ignore null values because null means that the value is not set 182 | // return nil 183 | // } 184 | // if errors.Is(err, tflint.ErrSensitive) { 185 | // // Ignore sensitive values 186 | // return nil 187 | // } 188 | // return err 189 | // } 190 | // ``` 191 | // 192 | // However, if the target is cty.Value, these errors will not be returned. 193 | // 194 | // Here are the types that can be passed as the target: string, int, bool, []string, 195 | // []int, []bool, map[string]string, map[string]int, map[string]bool, and cty.Value. 196 | // Passing any other type will result in a panic, but you can make an exception by 197 | // passing wantType as an option. 198 | // 199 | // ``` 200 | // type complexVal struct { 201 | // Key string `cty:"key"` 202 | // Enabled bool `cty:"enabled"` 203 | // } 204 | // 205 | // wantType := cty.List(cty.Object(map[string]cty.Type{ 206 | // "key": cty.String, 207 | // "enabled": cty.Bool, 208 | // })) 209 | // 210 | // var complexVals []complexVal 211 | // runner.EvaluateExpr(expr, &compleVals, &tflint.EvaluateExprOption{WantType: &wantType}) 212 | // ``` 213 | // 214 | // For functions (callbacks), the assigned value is used as an argument to execute 215 | // the function. If a value cannot be assigned to the argument type, the execution 216 | // is skipped instead of returning an error. This is useful when it's always acceptable 217 | // to ignore exceptional values. 218 | // 219 | // Here's an example of how you can pass a function to EvaluateExpr: 220 | // 221 | // ``` 222 | // runner.EvaluateExpr(expr, func (val string) error { 223 | // // Test value 224 | // }, nil) 225 | // ``` 226 | EvaluateExpr(expr hcl.Expression, target interface{}, option *EvaluateExprOption) error 227 | 228 | // EmitIssue sends an issue to TFLint. You need to pass the message of the issue and the range. 229 | EmitIssue(rule Rule, message string, issueRange hcl.Range) error 230 | 231 | // EmitIssueWithFix is similar to EmitIssue, but it also supports autofix. 232 | // If you pass a function that rewrites the source code to the last argument, 233 | // TFLint will apply the fix when the --fix option is specified. 234 | // 235 | // The function is passed a tflint.Fixer that can be used to rewrite the source code. 236 | // See the tflint.Fixer interface for more details. 237 | // 238 | // Issues emitted using this function are automatically marked as fixable. 239 | // However, if you don't want to support autofix only under certain conditions (e.g. JSON syntax), 240 | // you can return tflint.ErrFixNotSupported from the fix function. 241 | // In this case, the issue will not be marked as fixable and the fix will not be applied. 242 | // 243 | // As a best practice for autofix, we recommend minimizing the amount of code changed at once. 244 | // If fixes for the same range conflict within the same rule, Fixer will return an error. 245 | EmitIssueWithFix(rule Rule, message string, issueRange hcl.Range, fixFunc func(f Fixer) error) error 246 | 247 | // EnsureNoError is a helper for error handling. Depending on the type of error generated by EvaluateExpr, 248 | // determine whether to exit, skip, or continue. If it is continued, the passed function will be executed. 249 | // 250 | // Deprecated: Use EvaluateExpr with a function callback. e.g. EvaluateExpr(expr, func (val T) error {}, ...) 251 | EnsureNoError(error, func() error) error 252 | } 253 | 254 | // Fixer is a tool to rewrite HCL source code. 255 | // The actual implementation is in the internal.Fixer. 256 | type Fixer interface { 257 | // ReplaceText rewrites the given range of source code to a new text. 258 | // If the range is overlapped with a previous rewrite range, it returns an error. 259 | // 260 | // Either string or tflint.TextNode is valid as an argument. 261 | // TextNode can be obtained with fixer.TextAt(range). 262 | // If the argument is a TextNode, and the range is contained in the replacement range, 263 | // this function automatically minimizes the replacement range as much as possible. 264 | // 265 | // For example, if the source code is "(foo)", ReplaceText(range, "[foo]") 266 | // rewrites the whole "(foo)". But ReplaceText(range, "[", TextAt(fooRange), "]") 267 | // rewrites only "(" and ")". This is useful to avoid unintended conflicts. 268 | ReplaceText(hcl.Range, ...any) error 269 | 270 | // InsertTextBefore inserts the given text before the given range. 271 | InsertTextBefore(hcl.Range, string) error 272 | 273 | // InsertTextAfter inserts the given text after the given range. 274 | InsertTextAfter(hcl.Range, string) error 275 | 276 | // Remove removes the given range of source code. 277 | Remove(hcl.Range) error 278 | 279 | // RemoveAttribute removes the given attribute from the source code. 280 | // The difference from Remove is that it removes the attribute 281 | // and the associated newlines, indentations, and comments. 282 | // This only works for HCL native syntax. JSON syntax is not supported 283 | // and returns tflint.ErrFixNotSupported. 284 | RemoveAttribute(*hcl.Attribute) error 285 | 286 | // RemoveBlock removes the given block from the source code. 287 | // The difference from Remove is that it removes the block 288 | // and the associated newlines, indentations, and comments. 289 | // This only works for HCL native syntax. JSON syntax is not supported 290 | // and returns tflint.ErrFixNotSupported. 291 | RemoveBlock(*hcl.Block) error 292 | 293 | // RemoveExtBlock removes the given block from the source code. 294 | // This is similar to RemoveBlock, but it works for hclext.Block. 295 | RemoveExtBlock(*hclext.Block) error 296 | 297 | // TextAt returns a text node at the given range. 298 | // This is expected to be passed as an argument to ReplaceText. 299 | // Note this doesn't take into account the changes made by the fixer in a rule. 300 | TextAt(hcl.Range) TextNode 301 | 302 | // ValueText returns a text representation of the given cty.Value. 303 | // Values are always converted to a single line. For more pretty-printing, 304 | // implement your own conversion function. 305 | // 306 | // This function is inspired by hclwrite.TokensForValue. 307 | // https://github.com/hashicorp/hcl/blob/v2.16.2/hclwrite/generate.go#L26 308 | ValueText(cty.Value) string 309 | 310 | // RangeTo returns a range from the given start position to the given text. 311 | // Note that it doesn't check if the text is actually in the range. 312 | RangeTo(to string, filename string, start hcl.Pos) hcl.Range 313 | } 314 | 315 | // Rule is the interface that the plugin's rules should satisfy. 316 | type Rule interface { 317 | // Name will be displayed with a message of an issue and will be the identifier used to control 318 | // the behavior of this rule in the configuration file etc. 319 | // Therefore, it is expected that this will not duplicate the rule names provided by other plugins. 320 | Name() string 321 | 322 | // Enabled indicates whether the rule is enabled by default. 323 | Enabled() bool 324 | 325 | // Severity indicates the severity of the rule. 326 | Severity() Severity 327 | 328 | // Link allows you to add a reference link to the rule. 329 | Link() string 330 | 331 | // Metadata allows you to set any metadata to the rule. 332 | // This value is never referenced by the SDK and can be used for your custom ruleset. 333 | Metadata() interface{} 334 | 335 | // Check is the entrypoint of the rule. You can fetch Terraform configurations and send issues via Runner. 336 | Check(Runner) error 337 | 338 | // All rules must embed the default rule. 339 | mustEmbedDefaultRule() 340 | } 341 | --------------------------------------------------------------------------------