github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/plugin/internal/plugin2host/plugin2host_test.go (about)

     1  package plugin2host
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"reflect"
     7  	"testing"
     8  
     9  	"github.com/google/go-cmp/cmp"
    10  	"github.com/google/go-cmp/cmp/cmpopts"
    11  	"github.com/hashicorp/go-plugin"
    12  	"github.com/hashicorp/hcl/v2"
    13  	"github.com/hashicorp/hcl/v2/hclsyntax"
    14  	"github.com/hashicorp/hcl/v2/json"
    15  	"github.com/terraform-linters/tflint-plugin-sdk/hclext"
    16  	"github.com/terraform-linters/tflint-plugin-sdk/internal"
    17  	"github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/proto"
    18  	"github.com/terraform-linters/tflint-plugin-sdk/terraform/addrs"
    19  	"github.com/terraform-linters/tflint-plugin-sdk/terraform/lang/marks"
    20  	"github.com/terraform-linters/tflint-plugin-sdk/tflint"
    21  	"github.com/zclconf/go-cty/cty"
    22  	"google.golang.org/grpc"
    23  )
    24  
    25  func startTestGRPCServer(t *testing.T, runner Server) *GRPCClient {
    26  	conn, _ := plugin.TestGRPCConn(t, func(server *grpc.Server) {
    27  		proto.RegisterRunnerServer(server, &GRPCServer{Impl: runner})
    28  	})
    29  
    30  	return &GRPCClient{
    31  		Client:     proto.NewRunnerClient(conn),
    32  		Fixer:      internal.NewFixer(runner.GetFiles(tflint.RootModuleCtxType)),
    33  		FixEnabled: false,
    34  	}
    35  }
    36  
    37  var _ Server = &mockServer{}
    38  
    39  type mockServer struct {
    40  	impl mockServerImpl
    41  }
    42  
    43  type mockServerImpl struct {
    44  	getOriginalwd        func() string
    45  	getModulePath        func() []string
    46  	getModuleContent     func(*hclext.BodySchema, tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics)
    47  	getFile              func(string) (*hcl.File, error)
    48  	getFiles             func() map[string][]byte
    49  	getRuleConfigContent func(string, *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error)
    50  	evaluateExpr         func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error)
    51  	emitIssue            func(tflint.Rule, string, hcl.Range, bool) (bool, error)
    52  	applyChanges         func(map[string][]byte) error
    53  }
    54  
    55  func newMockServer(impl mockServerImpl) *mockServer {
    56  	return &mockServer{impl: impl}
    57  }
    58  
    59  func (s *mockServer) GetOriginalwd() string {
    60  	if s.impl.getOriginalwd != nil {
    61  		return s.impl.getOriginalwd()
    62  	}
    63  	return ""
    64  }
    65  
    66  func (s *mockServer) GetModulePath() []string {
    67  	if s.impl.getModulePath != nil {
    68  		return s.impl.getModulePath()
    69  	}
    70  	return []string{}
    71  }
    72  
    73  func (s *mockServer) GetModuleContent(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
    74  	if s.impl.getModuleContent != nil {
    75  		return s.impl.getModuleContent(schema, opts)
    76  	}
    77  	return &hclext.BodyContent{}, hcl.Diagnostics{}
    78  }
    79  
    80  func (s *mockServer) GetFile(filename string) (*hcl.File, error) {
    81  	if s.impl.getFile != nil {
    82  		return s.impl.getFile(filename)
    83  	}
    84  	return nil, nil
    85  }
    86  
    87  func (s *mockServer) GetFiles(tflint.ModuleCtxType) map[string][]byte {
    88  	if s.impl.getFiles != nil {
    89  		return s.impl.getFiles()
    90  	}
    91  	return map[string][]byte{}
    92  }
    93  
    94  func (s *mockServer) GetRuleConfigContent(name string, schema *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) {
    95  	if s.impl.getRuleConfigContent != nil {
    96  		return s.impl.getRuleConfigContent(name, schema)
    97  	}
    98  	return &hclext.BodyContent{}, map[string][]byte{}, nil
    99  }
   100  
   101  func (s *mockServer) EvaluateExpr(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
   102  	if s.impl.evaluateExpr != nil {
   103  		return s.impl.evaluateExpr(expr, opts)
   104  	}
   105  	return cty.Value{}, nil
   106  }
   107  
   108  func (s *mockServer) EmitIssue(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) {
   109  	if s.impl.emitIssue != nil {
   110  		return s.impl.emitIssue(rule, message, location, fixable)
   111  	}
   112  	return true, nil
   113  }
   114  
   115  func (s *mockServer) ApplyChanges(sources map[string][]byte) error {
   116  	if s.impl.applyChanges != nil {
   117  		return s.impl.applyChanges(sources)
   118  	}
   119  	return nil
   120  }
   121  
   122  // @see https://github.com/google/go-cmp/issues/40
   123  var allowAllUnexported = cmp.Exporter(func(reflect.Type) bool { return true })
   124  
   125  func TestGetOriginalwd(t *testing.T) {
   126  	tests := []struct {
   127  		Name       string
   128  		ServerImpl func() string
   129  		Want       string
   130  	}{
   131  		{
   132  			Name: "get the original working directory",
   133  			ServerImpl: func() string {
   134  				return "/work"
   135  			},
   136  			Want: "/work",
   137  		},
   138  	}
   139  
   140  	for _, test := range tests {
   141  		t.Run(test.Name, func(t *testing.T) {
   142  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{getOriginalwd: test.ServerImpl}))
   143  
   144  			got, err := client.GetOriginalwd()
   145  			if err != nil {
   146  				t.Fatalf("failed to call GetOriginalwd: %s", err)
   147  			}
   148  			if diff := cmp.Diff(got, test.Want); diff != "" {
   149  				t.Errorf("diff: %s", diff)
   150  			}
   151  		})
   152  	}
   153  }
   154  
   155  func TestGetModulePath(t *testing.T) {
   156  	tests := []struct {
   157  		Name       string
   158  		ServerImpl func() []string
   159  		Want       addrs.Module
   160  	}{
   161  		{
   162  			Name: "get root module path",
   163  			ServerImpl: func() []string {
   164  				return []string{}
   165  			},
   166  			Want: nil,
   167  		},
   168  		{
   169  			Name: "get child module path",
   170  			ServerImpl: func() []string {
   171  				return []string{"child1", "child2"}
   172  			},
   173  			Want: []string{"child1", "child2"},
   174  		},
   175  	}
   176  
   177  	for _, test := range tests {
   178  		t.Run(test.Name, func(t *testing.T) {
   179  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{getModulePath: test.ServerImpl}))
   180  
   181  			got, err := client.GetModulePath()
   182  			if err != nil {
   183  				t.Fatalf("failed to call GetModulePath: %s", err)
   184  			}
   185  			if diff := cmp.Diff(got, test.Want); diff != "" {
   186  				t.Errorf("diff: %s", diff)
   187  			}
   188  		})
   189  	}
   190  }
   191  
   192  func TestGetResourceContent(t *testing.T) {
   193  	// default error check helper
   194  	neverHappend := func(err error) bool { return err != nil }
   195  
   196  	// default getFileImpl function
   197  	files := map[string][]byte{}
   198  	fileExists := func() map[string][]byte {
   199  		return files
   200  	}
   201  
   202  	// test util functions
   203  	hclFile := func(filename string, code string) *hcl.File {
   204  		file, diags := hclsyntax.ParseConfig([]byte(code), filename, hcl.InitialPos)
   205  		if diags.HasErrors() {
   206  			panic(diags)
   207  		}
   208  		files[filename] = file.Bytes
   209  		return file
   210  	}
   211  	jsonFile := func(filename string, code string) *hcl.File {
   212  		file, diags := json.Parse([]byte(code), filename)
   213  		if diags.HasErrors() {
   214  			panic(diags)
   215  		}
   216  		files[filename] = file.Bytes
   217  		return file
   218  	}
   219  
   220  	tests := []struct {
   221  		Name       string
   222  		Args       func() (string, *hclext.BodySchema, *tflint.GetModuleContentOption)
   223  		ServerImpl func(*hclext.BodySchema, tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics)
   224  		Want       func(string, *hclext.BodySchema, *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics)
   225  		ErrCheck   func(error) bool
   226  	}{
   227  		{
   228  			Name: "get HCL content",
   229  			Args: func() (string, *hclext.BodySchema, *tflint.GetModuleContentOption) {
   230  				return "aws_instance", &hclext.BodySchema{
   231  					Attributes: []hclext.AttributeSchema{{Name: "instance_type"}},
   232  				}, nil
   233  			},
   234  			ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   235  				file := hclFile("test.tf", `
   236  resource "aws_instance" "foo" {
   237  	instance_type = "t2.micro"
   238  }
   239  
   240  resource "aws_s3_bucket" "bar" {
   241  	bucket = "test"
   242  }`)
   243  				return hclext.PartialContent(file.Body, schema)
   244  			},
   245  			Want: func(resource string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   246  				// Removed "aws_s3_bucket" resource
   247  				file := hclFile("test.tf", `
   248  resource "aws_instance" "foo" {
   249  	instance_type = "t2.micro"
   250  }`)
   251  				return hclext.Content(file.Body, &hclext.BodySchema{
   252  					Blocks: []hclext.BlockSchema{
   253  						{
   254  							Type:       "resource",
   255  							LabelNames: []string{"type", "name"},
   256  							Body: &hclext.BodySchema{
   257  								Attributes: []hclext.AttributeSchema{{Name: "instance_type"}},
   258  							},
   259  						},
   260  					},
   261  				})
   262  			},
   263  			ErrCheck: neverHappend,
   264  		},
   265  		{
   266  			Name: "get JSON content",
   267  			Args: func() (string, *hclext.BodySchema, *tflint.GetModuleContentOption) {
   268  				return "aws_instance", &hclext.BodySchema{
   269  					Attributes: []hclext.AttributeSchema{{Name: "instance_type"}},
   270  				}, nil
   271  			},
   272  			ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   273  				file := jsonFile("test.tf.json", `
   274  {
   275    "resource": {
   276      "aws_instance": {
   277        "foo": {
   278          "instance_type": "t2.micro"
   279        }
   280      },
   281  	"aws_s3_bucket": {
   282        "bar": {
   283          "bucket": "test"
   284  	  }
   285  	}
   286    }
   287  }`)
   288  				return hclext.PartialContent(file.Body, schema)
   289  			},
   290  			Want: func(resource string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   291  				// Removed "aws_s3_bucket" resource
   292  				file := jsonFile("test.tf.json", `
   293  {
   294    "resource": {
   295      "aws_instance": {
   296        "foo": {
   297          "instance_type": "t2.micro"
   298        }
   299      }
   300    }
   301  }`)
   302  				return hclext.Content(file.Body, &hclext.BodySchema{
   303  					Blocks: []hclext.BlockSchema{
   304  						{
   305  							Type:       "resource",
   306  							LabelNames: []string{"type", "name"},
   307  							Body: &hclext.BodySchema{
   308  								Attributes: []hclext.AttributeSchema{{Name: "instance_type"}},
   309  							},
   310  						},
   311  					},
   312  				})
   313  			},
   314  			ErrCheck: neverHappend,
   315  		},
   316  		{
   317  			Name: "get content with options",
   318  			Args: func() (string, *hclext.BodySchema, *tflint.GetModuleContentOption) {
   319  				return "aws_instance", &hclext.BodySchema{}, &tflint.GetModuleContentOption{
   320  					ModuleCtx: tflint.RootModuleCtxType,
   321  				}
   322  			},
   323  			ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   324  				if opts.ModuleCtx != tflint.RootModuleCtxType {
   325  					return &hclext.BodyContent{}, hcl.Diagnostics{
   326  						&hcl.Diagnostic{Severity: hcl.DiagError, Summary: "unexpected moduleCtx options"},
   327  					}
   328  				}
   329  				if opts.Hint.ResourceType != "aws_instance" {
   330  					return &hclext.BodyContent{}, hcl.Diagnostics{
   331  						&hcl.Diagnostic{Severity: hcl.DiagError, Summary: "unexpected hint options"},
   332  					}
   333  				}
   334  				return &hclext.BodyContent{}, hcl.Diagnostics{}
   335  			},
   336  			Want: func(resource string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   337  				return &hclext.BodyContent{
   338  					Attributes: hclext.Attributes{},
   339  					Blocks:     hclext.Blocks{},
   340  				}, hcl.Diagnostics{}
   341  			},
   342  			ErrCheck: neverHappend,
   343  		},
   344  	}
   345  
   346  	for _, test := range tests {
   347  		t.Run(test.Name, func(t *testing.T) {
   348  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{getModuleContent: test.ServerImpl, getFiles: fileExists}))
   349  
   350  			got, err := client.GetResourceContent(test.Args())
   351  			if test.ErrCheck(err) {
   352  				t.Fatalf("failed to call GetResourceContent: %s", err)
   353  			}
   354  			want, diags := test.Want(test.Args())
   355  			if diags.HasErrors() {
   356  				t.Fatalf("failed to get want: %d diagsnotics", len(diags))
   357  				for _, diag := range diags {
   358  					t.Logf("  - %s", diag.Error())
   359  				}
   360  			}
   361  
   362  			opts := cmp.Options{
   363  				cmp.Comparer(func(x, y cty.Value) bool {
   364  					return x.GoString() == y.GoString()
   365  				}),
   366  				cmpopts.EquateEmpty(),
   367  				allowAllUnexported,
   368  			}
   369  			if diff := cmp.Diff(got, want, opts); diff != "" {
   370  				t.Errorf("diff: %s", diff)
   371  			}
   372  		})
   373  	}
   374  }
   375  
   376  func TestGetProviderContent(t *testing.T) {
   377  	// default error check helper
   378  	neverHappend := func(err error) bool { return err != nil }
   379  
   380  	// default getFileImpl function
   381  	files := map[string][]byte{}
   382  	fileExists := func() map[string][]byte {
   383  		return files
   384  	}
   385  
   386  	// test util functions
   387  	hclFile := func(filename string, code string) *hcl.File {
   388  		file, diags := hclsyntax.ParseConfig([]byte(code), filename, hcl.InitialPos)
   389  		if diags.HasErrors() {
   390  			panic(diags)
   391  		}
   392  		files[filename] = file.Bytes
   393  		return file
   394  	}
   395  
   396  	tests := []struct {
   397  		Name       string
   398  		Args       func() (string, *hclext.BodySchema, *tflint.GetModuleContentOption)
   399  		ServerImpl func(*hclext.BodySchema, tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics)
   400  		Want       func(string, *hclext.BodySchema, *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics)
   401  		ErrCheck   func(error) bool
   402  	}{
   403  		{
   404  			Name: "get HCL content",
   405  			Args: func() (string, *hclext.BodySchema, *tflint.GetModuleContentOption) {
   406  				return "aws", &hclext.BodySchema{
   407  					Attributes: []hclext.AttributeSchema{{Name: "region"}},
   408  				}, nil
   409  			},
   410  			ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   411  				file := hclFile("test.tf", `
   412  provider "aws" {
   413    region = "us-east-1"
   414  }
   415  
   416  provider "google" {
   417    region = "us-central1"
   418  }`)
   419  				return hclext.PartialContent(file.Body, schema)
   420  			},
   421  			Want: func(resource string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   422  				// Removed "google" provider
   423  				file := hclFile("test.tf", `
   424  provider "aws" {
   425    region = "us-east-1"
   426  }`)
   427  				return hclext.Content(file.Body, &hclext.BodySchema{
   428  					Blocks: []hclext.BlockSchema{
   429  						{
   430  							Type:       "provider",
   431  							LabelNames: []string{"name"},
   432  							Body: &hclext.BodySchema{
   433  								Attributes: []hclext.AttributeSchema{{Name: "region"}},
   434  							},
   435  						},
   436  					},
   437  				})
   438  			},
   439  			ErrCheck: neverHappend,
   440  		},
   441  	}
   442  
   443  	for _, test := range tests {
   444  		t.Run(test.Name, func(t *testing.T) {
   445  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{getModuleContent: test.ServerImpl, getFiles: fileExists}))
   446  
   447  			got, err := client.GetProviderContent(test.Args())
   448  			if test.ErrCheck(err) {
   449  				t.Fatalf("failed to call GetProviderContent: %s", err)
   450  			}
   451  			want, diags := test.Want(test.Args())
   452  			if diags.HasErrors() {
   453  				t.Fatalf("failed to get want: %d diagsnotics", len(diags))
   454  				for _, diag := range diags {
   455  					t.Logf("  - %s", diag.Error())
   456  				}
   457  			}
   458  
   459  			opts := cmp.Options{
   460  				cmp.Comparer(func(x, y cty.Value) bool {
   461  					return x.GoString() == y.GoString()
   462  				}),
   463  				cmpopts.EquateEmpty(),
   464  				allowAllUnexported,
   465  			}
   466  			if diff := cmp.Diff(got, want, opts); diff != "" {
   467  				t.Errorf("diff: %s", diff)
   468  			}
   469  		})
   470  	}
   471  }
   472  
   473  func TestGetModuleContent(t *testing.T) {
   474  	// default error check helper
   475  	neverHappend := func(err error) bool { return err != nil }
   476  
   477  	// default getFileImpl function
   478  	files := map[string][]byte{}
   479  	fileExists := func() map[string][]byte {
   480  		return files
   481  	}
   482  
   483  	// test util functions
   484  	hclFile := func(filename string, code string) *hcl.File {
   485  		file, diags := hclsyntax.ParseConfig([]byte(code), filename, hcl.InitialPos)
   486  		if diags.HasErrors() {
   487  			panic(diags)
   488  		}
   489  		files[filename] = file.Bytes
   490  		return file
   491  	}
   492  	jsonFile := func(filename string, code string) *hcl.File {
   493  		file, diags := json.Parse([]byte(code), filename)
   494  		if diags.HasErrors() {
   495  			panic(diags)
   496  		}
   497  		files[filename] = file.Bytes
   498  		return file
   499  	}
   500  
   501  	tests := []struct {
   502  		Name       string
   503  		Args       func() (*hclext.BodySchema, *tflint.GetModuleContentOption)
   504  		ServerImpl func(*hclext.BodySchema, tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics)
   505  		Want       func(*hclext.BodySchema, *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics)
   506  		ErrCheck   func(error) bool
   507  	}{
   508  		{
   509  			Name: "get HCL content",
   510  			Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) {
   511  				return &hclext.BodySchema{
   512  					Blocks: []hclext.BlockSchema{
   513  						{
   514  							Type:       "resource",
   515  							LabelNames: []string{"type", "name"},
   516  							Body: &hclext.BodySchema{
   517  								Attributes: []hclext.AttributeSchema{{Name: "instance_type"}},
   518  							},
   519  						},
   520  					},
   521  				}, nil
   522  			},
   523  			ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   524  				file := hclFile("test.tf", `
   525  resource "aws_instance" "foo" {
   526  	instance_type = "t2.micro"
   527  }`)
   528  				return hclext.Content(file.Body, schema)
   529  			},
   530  			Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   531  				file := hclFile("test.tf", `
   532  resource "aws_instance" "foo" {
   533  	instance_type = "t2.micro"
   534  }`)
   535  				return hclext.Content(file.Body, schema)
   536  			},
   537  			ErrCheck: neverHappend,
   538  		},
   539  		{
   540  			Name: "get JSON content",
   541  			Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) {
   542  				return &hclext.BodySchema{
   543  					Blocks: []hclext.BlockSchema{
   544  						{
   545  							Type:       "resource",
   546  							LabelNames: []string{"type", "name"},
   547  							Body: &hclext.BodySchema{
   548  								Attributes: []hclext.AttributeSchema{{Name: "instance_type"}},
   549  							},
   550  						},
   551  					},
   552  				}, nil
   553  			},
   554  			ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   555  				file := jsonFile("test.tf.json", `
   556  {
   557    "resource": {
   558      "aws_instance": {
   559        "foo": {
   560          "instance_type": "t2.micro"
   561        }
   562      }
   563    }
   564  }`)
   565  				return hclext.Content(file.Body, schema)
   566  			},
   567  			Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   568  				file := jsonFile("test.tf.json", `
   569  {
   570    "resource": {
   571      "aws_instance": {
   572        "foo": {
   573          "instance_type": "t2.micro"
   574        }
   575      }
   576    }
   577  }`)
   578  				return hclext.Content(file.Body, schema)
   579  			},
   580  			ErrCheck: neverHappend,
   581  		},
   582  		{
   583  			Name: "get content as just attributes",
   584  			Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) {
   585  				return &hclext.BodySchema{Mode: hclext.SchemaJustAttributesMode}, nil
   586  			},
   587  			ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   588  				file := hclFile("test.tf", `
   589  instance_type = "t2.micro"
   590  volume_size = 10`)
   591  				return hclext.Content(file.Body, schema)
   592  			},
   593  			Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   594  				file := hclFile("test.tf", `
   595  instance_type = "t2.micro"
   596  volume_size = 10`)
   597  				return hclext.Content(file.Body, schema)
   598  			},
   599  			ErrCheck: neverHappend,
   600  		},
   601  		{
   602  			Name: "get content with bound expr",
   603  			Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) {
   604  				return &hclext.BodySchema{
   605  					Attributes: []hclext.AttributeSchema{{Name: "value"}},
   606  				}, nil
   607  			},
   608  			ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   609  				file := hclFile("test.tf", "value = each.key")
   610  				attrs, diags := file.Body.JustAttributes()
   611  				if diags.HasErrors() {
   612  					return nil, diags
   613  				}
   614  				attr := attrs["value"]
   615  
   616  				return &hclext.BodyContent{
   617  					Attributes: hclext.Attributes{
   618  						"value": {
   619  							Name:      attr.Name,
   620  							Expr:      hclext.BindValue(cty.StringVal("bound value"), attr.Expr),
   621  							Range:     attr.Range,
   622  							NameRange: attr.NameRange,
   623  						},
   624  					},
   625  					Blocks: hclext.Blocks{},
   626  				}, nil
   627  			},
   628  			Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   629  				file := hclFile("test.tf", "value = each.key")
   630  				attrs, diags := file.Body.JustAttributes()
   631  				if diags.HasErrors() {
   632  					return nil, diags
   633  				}
   634  				attr := attrs["value"]
   635  
   636  				return &hclext.BodyContent{
   637  					Attributes: hclext.Attributes{
   638  						"value": {
   639  							Name:      attr.Name,
   640  							Expr:      hclext.BindValue(cty.StringVal("bound value"), attr.Expr),
   641  							Range:     attr.Range,
   642  							NameRange: attr.NameRange,
   643  						},
   644  					},
   645  					Blocks: hclext.Blocks{},
   646  				}, nil
   647  			},
   648  			ErrCheck: neverHappend,
   649  		},
   650  		{
   651  			Name: "get content with options",
   652  			Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) {
   653  				return &hclext.BodySchema{}, &tflint.GetModuleContentOption{
   654  					ModuleCtx:  tflint.RootModuleCtxType,
   655  					ExpandMode: tflint.ExpandModeNone,
   656  					Hint:       tflint.GetModuleContentHint{ResourceType: "aws_instance"},
   657  				}
   658  			},
   659  			ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   660  				if opts.ModuleCtx != tflint.RootModuleCtxType {
   661  					return &hclext.BodyContent{}, hcl.Diagnostics{
   662  						&hcl.Diagnostic{Severity: hcl.DiagError, Summary: "unexpected moduleCtx options"},
   663  					}
   664  				}
   665  				if opts.ExpandMode != tflint.ExpandModeNone {
   666  					return &hclext.BodyContent{}, hcl.Diagnostics{
   667  						&hcl.Diagnostic{Severity: hcl.DiagError, Summary: "unexpected expand mode options"},
   668  					}
   669  				}
   670  				if opts.Hint.ResourceType != "aws_instance" {
   671  					return &hclext.BodyContent{}, hcl.Diagnostics{
   672  						&hcl.Diagnostic{Severity: hcl.DiagError, Summary: "unexpected hint options"},
   673  					}
   674  				}
   675  				return &hclext.BodyContent{}, hcl.Diagnostics{}
   676  			},
   677  			Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   678  				return &hclext.BodyContent{
   679  					Attributes: hclext.Attributes{},
   680  					Blocks:     hclext.Blocks{},
   681  				}, hcl.Diagnostics{}
   682  			},
   683  			ErrCheck: neverHappend,
   684  		},
   685  		{
   686  			Name: "schema is null",
   687  			Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) {
   688  				return nil, nil
   689  			},
   690  			Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   691  				return &hclext.BodyContent{
   692  					Attributes: hclext.Attributes{},
   693  					Blocks:     hclext.Blocks{},
   694  				}, hcl.Diagnostics{}
   695  			},
   696  			ErrCheck: neverHappend,
   697  		},
   698  		{
   699  			Name: "server returns an error",
   700  			Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) {
   701  				return &hclext.BodySchema{}, nil
   702  			},
   703  			ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   704  				return &hclext.BodyContent{}, hcl.Diagnostics{
   705  					&hcl.Diagnostic{
   706  						Severity: hcl.DiagError,
   707  						Summary:  "unexpected error",
   708  						Detail:   "unexpected error occurred",
   709  						Subject:  &hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 1, Column: 1}, End: hcl.Pos{Line: 1, Column: 5}},
   710  					},
   711  				}
   712  			},
   713  			Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   714  				return nil, hcl.Diagnostics{}
   715  			},
   716  			ErrCheck: func(err error) bool {
   717  				return err == nil || err.Error() != "test.tf:1,1-5: unexpected error; unexpected error occurred"
   718  			},
   719  		},
   720  		{
   721  			Name: "response body is empty",
   722  			Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) {
   723  				return &hclext.BodySchema{}, nil
   724  			},
   725  			ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   726  				return nil, hcl.Diagnostics{}
   727  			},
   728  			Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
   729  				return nil, hcl.Diagnostics{}
   730  			},
   731  			ErrCheck: func(err error) bool {
   732  				return err == nil || err.Error() != "response body is empty"
   733  			},
   734  		},
   735  	}
   736  
   737  	for _, test := range tests {
   738  		t.Run(test.Name, func(t *testing.T) {
   739  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{getModuleContent: test.ServerImpl, getFiles: fileExists}))
   740  
   741  			got, err := client.GetModuleContent(test.Args())
   742  			if test.ErrCheck(err) {
   743  				t.Fatalf("failed to call GetModuleContent: %s", err)
   744  			}
   745  			want, diags := test.Want(test.Args())
   746  			if diags.HasErrors() {
   747  				t.Fatalf("failed to get want: %d diagsnotics", len(diags))
   748  				for _, diag := range diags {
   749  					t.Logf("  - %s", diag.Error())
   750  				}
   751  			}
   752  
   753  			opts := cmp.Options{
   754  				cmp.Comparer(func(x, y cty.Value) bool {
   755  					return x.GoString() == y.GoString()
   756  				}),
   757  				allowAllUnexported,
   758  			}
   759  			if diff := cmp.Diff(got, want, opts); diff != "" {
   760  				t.Errorf("diff: %s", diff)
   761  			}
   762  		})
   763  	}
   764  }
   765  
   766  func TestGetFile(t *testing.T) {
   767  	// default error check helper
   768  	neverHappend := func(err error) bool { return err != nil }
   769  
   770  	// test util functions
   771  	hclFile := func(filename string, code string) (*hcl.File, error) {
   772  		file, diags := hclsyntax.ParseConfig([]byte(code), filename, hcl.InitialPos)
   773  		if diags.HasErrors() {
   774  			return nil, diags
   775  		}
   776  		return file, nil
   777  	}
   778  	jsonFile := func(filename string, code string) (*hcl.File, error) {
   779  		file, diags := json.Parse([]byte(code), filename)
   780  		if diags.HasErrors() {
   781  			return nil, diags
   782  		}
   783  		return file, nil
   784  	}
   785  
   786  	tests := []struct {
   787  		Name       string
   788  		Arg        string
   789  		ServerImpl func(string) (*hcl.File, error)
   790  		Want       string
   791  		ErrCheck   func(error) bool
   792  	}{
   793  		{
   794  			Name: "HCL file exists",
   795  			Arg:  "test.tf",
   796  			ServerImpl: func(filename string) (*hcl.File, error) {
   797  				if filename != "test.tf" {
   798  					return nil, nil
   799  				}
   800  				return hclFile(filename, `
   801  resource "aws_instance" "foo" {
   802  	instance_type = "t2.micro"
   803  }`)
   804  			},
   805  			Want: `
   806  resource "aws_instance" "foo" {
   807  	instance_type = "t2.micro"
   808  }`,
   809  			ErrCheck: neverHappend,
   810  		},
   811  		{
   812  			Name: "JSON file exists",
   813  			Arg:  "test.tf.json",
   814  			ServerImpl: func(filename string) (*hcl.File, error) {
   815  				if filename != "test.tf.json" {
   816  					return nil, nil
   817  				}
   818  				return jsonFile(filename, `
   819  {
   820    "resource": {
   821      "aws_instance": {
   822        "foo": {
   823          "instance_type": "t2.micro"
   824        }
   825      }
   826    }
   827  }`)
   828  			},
   829  			Want: `
   830  {
   831    "resource": {
   832      "aws_instance": {
   833        "foo": {
   834          "instance_type": "t2.micro"
   835        }
   836      }
   837    }
   838  }`,
   839  			ErrCheck: neverHappend,
   840  		},
   841  		{
   842  			Name: "file not found",
   843  			Arg:  "test.tf",
   844  			ServerImpl: func(filename string) (*hcl.File, error) {
   845  				return nil, nil
   846  			},
   847  			ErrCheck: func(err error) bool {
   848  				return err == nil || err.Error() != "file not found"
   849  			},
   850  		},
   851  		{
   852  			Name: "server returns an error",
   853  			Arg:  "test.tf",
   854  			ServerImpl: func(filename string) (*hcl.File, error) {
   855  				if filename != "test.tf" {
   856  					return nil, nil
   857  				}
   858  				return nil, errors.New("unexpected error")
   859  			},
   860  			ErrCheck: func(err error) bool {
   861  				return err == nil || err.Error() != "unexpected error"
   862  			},
   863  		},
   864  		{
   865  			Name: "filename is empty",
   866  			Arg:  "",
   867  			ErrCheck: func(err error) bool {
   868  				return err == nil || err.Error() != "name should not be empty"
   869  			},
   870  		},
   871  	}
   872  
   873  	for _, test := range tests {
   874  		t.Run(test.Name, func(t *testing.T) {
   875  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{getFile: test.ServerImpl}))
   876  
   877  			file, err := client.GetFile(test.Arg)
   878  			if test.ErrCheck(err) {
   879  				t.Fatalf("failed to call GetFile: %s", err)
   880  			}
   881  
   882  			var got string
   883  			if file != nil {
   884  				got = string(file.Bytes)
   885  			}
   886  
   887  			if got != test.Want {
   888  				t.Errorf("got: %s", got)
   889  			}
   890  		})
   891  	}
   892  }
   893  
   894  func TestGetFiles(t *testing.T) {
   895  	// default error check helper
   896  	neverHappend := func(err error) bool { return err != nil }
   897  
   898  	// test util functions
   899  	hclFile := func(filename string, code string) *hcl.File {
   900  		file, diags := hclsyntax.ParseConfig([]byte(code), filename, hcl.InitialPos)
   901  		if diags.HasErrors() {
   902  			panic(diags)
   903  		}
   904  		return file
   905  	}
   906  	jsonFile := func(filename string, code string) *hcl.File {
   907  		file, diags := json.Parse([]byte(code), filename)
   908  		if diags.HasErrors() {
   909  			panic(diags)
   910  		}
   911  		return file
   912  	}
   913  
   914  	tests := []struct {
   915  		Name       string
   916  		ServerImpl func() map[string][]byte
   917  		Want       map[string]*hcl.File
   918  		ErrCheck   func(error) bool
   919  	}{
   920  		{
   921  			Name: "HCL files",
   922  			ServerImpl: func() map[string][]byte {
   923  				return map[string][]byte{
   924  					"test1.tf": []byte(`
   925  resource "aws_instance" "foo" {
   926  	instance_type = "t2.micro"
   927  }`),
   928  					"test2.tf": []byte(`
   929  resource "aws_s3_bucket" "bar" {
   930  	bucket = "baz"
   931  }`),
   932  				}
   933  			},
   934  			Want: map[string]*hcl.File{
   935  				"test1.tf": hclFile("test1.tf", `
   936  resource "aws_instance" "foo" {
   937  	instance_type = "t2.micro"
   938  }`),
   939  				"test2.tf": hclFile("test2.tf", `
   940  resource "aws_s3_bucket" "bar" {
   941  	bucket = "baz"
   942  }`),
   943  			},
   944  			ErrCheck: neverHappend,
   945  		},
   946  		{
   947  			Name: "JSON files",
   948  			ServerImpl: func() map[string][]byte {
   949  				return map[string][]byte{
   950  					"test1.tf.json": []byte(`
   951  {
   952    "resource": {
   953      "aws_instance": {
   954        "foo": {
   955          "instance_type": "t2.micro"
   956        }
   957      }
   958    }
   959  }`),
   960  					"test2.tf.json": []byte(`
   961  {
   962    "resource": {
   963      "aws_s3_bucket": {
   964        "bar": {
   965          "bucket": "baz"
   966        }
   967      }
   968    }
   969  }`),
   970  				}
   971  			},
   972  			Want: map[string]*hcl.File{
   973  				"test1.tf.json": jsonFile("test1.tf.json", `
   974  {
   975    "resource": {
   976      "aws_instance": {
   977        "foo": {
   978          "instance_type": "t2.micro"
   979        }
   980      }
   981    }
   982  }`),
   983  				"test2.tf.json": jsonFile("test2.tf.json", `
   984  {
   985    "resource": {
   986      "aws_s3_bucket": {
   987        "bar": {
   988          "bucket": "baz"
   989        }
   990      }
   991    }
   992  }`),
   993  			},
   994  			ErrCheck: neverHappend,
   995  		},
   996  	}
   997  
   998  	for _, test := range tests {
   999  		t.Run(test.Name, func(t *testing.T) {
  1000  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{getFiles: test.ServerImpl}))
  1001  
  1002  			files, err := client.GetFiles()
  1003  			if test.ErrCheck(err) {
  1004  				t.Fatalf("failed to call GetFiles: %s", err)
  1005  			}
  1006  
  1007  			opts := cmp.Options{
  1008  				cmp.Comparer(func(x, y cty.Value) bool {
  1009  					return x.GoString() == y.GoString()
  1010  				}),
  1011  				cmp.AllowUnexported(hclsyntax.Body{}),
  1012  				cmpopts.IgnoreFields(hcl.File{}, "Nav"),
  1013  				allowAllUnexported,
  1014  			}
  1015  			if diff := cmp.Diff(files, test.Want, opts); diff != "" {
  1016  				t.Errorf("diff: %s", diff)
  1017  			}
  1018  		})
  1019  	}
  1020  }
  1021  
  1022  func TestWalkExpressions(t *testing.T) {
  1023  	tests := []struct {
  1024  		name   string
  1025  		files  map[string][]byte
  1026  		walked []hcl.Range
  1027  	}{
  1028  		{
  1029  			name: "resource",
  1030  			files: map[string][]byte{
  1031  				"resource.tf": []byte(`
  1032  resource "null_resource" "test" {
  1033    key = "foo"
  1034  }`),
  1035  			},
  1036  			walked: []hcl.Range{
  1037  				{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
  1038  				{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
  1039  			},
  1040  		},
  1041  		{
  1042  			name: "data source",
  1043  			files: map[string][]byte{
  1044  				"data.tf": []byte(`
  1045  data "null_dataresource" "test" {
  1046    key = "foo"
  1047  }`),
  1048  			},
  1049  			walked: []hcl.Range{
  1050  				{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
  1051  				{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
  1052  			},
  1053  		},
  1054  		{
  1055  			name: "module call",
  1056  			files: map[string][]byte{
  1057  				"module.tf": []byte(`
  1058  module "m" {
  1059    source = "./module"
  1060    key    = "foo"
  1061  }`),
  1062  			},
  1063  			walked: []hcl.Range{
  1064  				{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 22}},
  1065  				{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 21}},
  1066  				{Start: hcl.Pos{Line: 4, Column: 12}, End: hcl.Pos{Line: 4, Column: 17}},
  1067  				{Start: hcl.Pos{Line: 4, Column: 13}, End: hcl.Pos{Line: 4, Column: 16}},
  1068  			},
  1069  		},
  1070  		{
  1071  			name: "provider config",
  1072  			files: map[string][]byte{
  1073  				"provider.tf": []byte(`
  1074  provider "p" {
  1075    key = "foo"
  1076  }`),
  1077  			},
  1078  			walked: []hcl.Range{
  1079  				{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
  1080  				{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
  1081  			},
  1082  		},
  1083  		{
  1084  			name: "locals",
  1085  			files: map[string][]byte{
  1086  				"locals.tf": []byte(`
  1087  locals {
  1088    key = "foo"
  1089  }`),
  1090  			},
  1091  			walked: []hcl.Range{
  1092  				{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
  1093  				{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
  1094  			},
  1095  		},
  1096  		{
  1097  			name: "output",
  1098  			files: map[string][]byte{
  1099  				"output.tf": []byte(`
  1100  output "o" {
  1101    value = "foo"
  1102  }`),
  1103  			},
  1104  			walked: []hcl.Range{
  1105  				{Start: hcl.Pos{Line: 3, Column: 11}, End: hcl.Pos{Line: 3, Column: 16}},
  1106  				{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 15}},
  1107  			},
  1108  		},
  1109  		{
  1110  			name: "resource with block",
  1111  			files: map[string][]byte{
  1112  				"resource.tf": []byte(`
  1113  resource "null_resource" "test" {
  1114    key = "foo"
  1115  
  1116    lifecycle {
  1117      ignore_changes = [key]
  1118    }
  1119  }`),
  1120  			},
  1121  			walked: []hcl.Range{
  1122  				{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
  1123  				{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
  1124  				{Start: hcl.Pos{Line: 6, Column: 22}, End: hcl.Pos{Line: 6, Column: 27}},
  1125  				{Start: hcl.Pos{Line: 6, Column: 23}, End: hcl.Pos{Line: 6, Column: 26}},
  1126  			},
  1127  		},
  1128  		{
  1129  			name: "resource json",
  1130  			files: map[string][]byte{
  1131  				"resource.tf.json": []byte(`
  1132  {
  1133    "resource": {
  1134      "null_resource": {
  1135        "test": {
  1136          "key": "foo",
  1137          "nested": {
  1138            "key": "foo"
  1139          },
  1140          "list": [{
  1141            "key": "foo"
  1142          }]
  1143        }
  1144      }
  1145    }
  1146  }`),
  1147  			},
  1148  			walked: []hcl.Range{
  1149  				{Start: hcl.Pos{Line: 3, Column: 15}, End: hcl.Pos{Line: 15, Column: 4}},
  1150  			},
  1151  		},
  1152  		{
  1153  			name: "multiple files",
  1154  			files: map[string][]byte{
  1155  				"main.tf": []byte(`
  1156  provider "aws" {
  1157    region = "us-east-1"
  1158  
  1159    assume_role {
  1160      role_arn = "arn:aws:iam::123412341234:role/ExampleRole"
  1161    }
  1162  }`),
  1163  				"main_override.tf": []byte(`
  1164  provider "aws" {
  1165    region = "us-east-1"
  1166  
  1167    assume_role {
  1168      role_arn = null
  1169    }
  1170  }`),
  1171  			},
  1172  			walked: []hcl.Range{
  1173  				{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 23}, Filename: "main.tf"},
  1174  				{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 22}, Filename: "main.tf"},
  1175  				{Start: hcl.Pos{Line: 6, Column: 16}, End: hcl.Pos{Line: 6, Column: 60}, Filename: "main.tf"},
  1176  				{Start: hcl.Pos{Line: 6, Column: 17}, End: hcl.Pos{Line: 6, Column: 59}, Filename: "main.tf"},
  1177  				{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 23}, Filename: "main_override.tf"},
  1178  				{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 22}, Filename: "main_override.tf"},
  1179  				{Start: hcl.Pos{Line: 6, Column: 16}, End: hcl.Pos{Line: 6, Column: 20}, Filename: "main_override.tf"},
  1180  			},
  1181  		},
  1182  		{
  1183  			name: "nested attributes",
  1184  			files: map[string][]byte{
  1185  				"data.tf": []byte(`
  1186  data "terraform_remote_state" "remote_state" {
  1187    backend = "remote"
  1188  
  1189    config = {
  1190      organization = "Organization"
  1191      workspaces = {
  1192        name = "${var.environment}"
  1193      }
  1194    }
  1195  }`),
  1196  			},
  1197  			walked: []hcl.Range{
  1198  				{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 21}},
  1199  				{Start: hcl.Pos{Line: 3, Column: 14}, End: hcl.Pos{Line: 3, Column: 20}},
  1200  				{Start: hcl.Pos{Line: 5, Column: 12}, End: hcl.Pos{Line: 10, Column: 4}},
  1201  				{Start: hcl.Pos{Line: 6, Column: 5}, End: hcl.Pos{Line: 6, Column: 17}},
  1202  				{Start: hcl.Pos{Line: 6, Column: 20}, End: hcl.Pos{Line: 6, Column: 34}},
  1203  				{Start: hcl.Pos{Line: 6, Column: 21}, End: hcl.Pos{Line: 6, Column: 33}},
  1204  				{Start: hcl.Pos{Line: 7, Column: 5}, End: hcl.Pos{Line: 7, Column: 15}},
  1205  				{Start: hcl.Pos{Line: 7, Column: 18}, End: hcl.Pos{Line: 9, Column: 6}},
  1206  				{Start: hcl.Pos{Line: 8, Column: 7}, End: hcl.Pos{Line: 8, Column: 11}},
  1207  				{Start: hcl.Pos{Line: 8, Column: 14}, End: hcl.Pos{Line: 8, Column: 34}},
  1208  				{Start: hcl.Pos{Line: 8, Column: 17}, End: hcl.Pos{Line: 8, Column: 32}},
  1209  			},
  1210  		},
  1211  	}
  1212  
  1213  	for _, test := range tests {
  1214  		t.Run(test.name, func(t *testing.T) {
  1215  			getFilesImpl := func() map[string][]byte { return test.files }
  1216  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{getFiles: getFilesImpl}))
  1217  
  1218  			walked := []hcl.Range{}
  1219  			diags := client.WalkExpressions(tflint.ExprWalkFunc(func(expr hcl.Expression) hcl.Diagnostics {
  1220  				walked = append(walked, expr.Range())
  1221  				return nil
  1222  			}))
  1223  			if diags.HasErrors() {
  1224  				t.Fatal(diags)
  1225  			}
  1226  			opts := cmp.Options{
  1227  				cmpopts.IgnoreFields(hcl.Range{}, "Filename"),
  1228  				cmpopts.IgnoreFields(hcl.Pos{}, "Byte"),
  1229  				cmpopts.SortSlices(func(x, y hcl.Range) bool { return x.String() > y.String() }),
  1230  			}
  1231  			if diff := cmp.Diff(walked, test.walked, opts); diff != "" {
  1232  				t.Error(diff)
  1233  			}
  1234  		})
  1235  	}
  1236  }
  1237  
  1238  func TestDecodeRuleConfig(t *testing.T) {
  1239  	// default error check helper
  1240  	neverHappend := func(err error) bool { return err != nil }
  1241  
  1242  	// test struct for decoding
  1243  	type ruleConfig struct {
  1244  		Name string `hclext:"name"`
  1245  	}
  1246  
  1247  	tests := []struct {
  1248  		Name       string
  1249  		RuleName   string
  1250  		Target     interface{}
  1251  		ServerImpl func(string, *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error)
  1252  		Want       interface{}
  1253  		ErrCheck   func(error) bool
  1254  	}{
  1255  		{
  1256  			Name:     "decode to struct",
  1257  			RuleName: "test_rule",
  1258  			Target:   &ruleConfig{},
  1259  			ServerImpl: func(name string, schema *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) {
  1260  				if name != "test_rule" {
  1261  					return &hclext.BodyContent{}, map[string][]byte{}, errors.New("unexpected file name")
  1262  				}
  1263  
  1264  				sources := map[string][]byte{
  1265  					".tflint.hcl": []byte(`
  1266  rule "test_rule" {
  1267  	name = "foo"
  1268  }`),
  1269  				}
  1270  
  1271  				file, diags := hclsyntax.ParseConfig(sources[".tflint.hcl"], ".tflint.hcl", hcl.InitialPos)
  1272  				if diags.HasErrors() {
  1273  					return &hclext.BodyContent{}, sources, diags
  1274  				}
  1275  
  1276  				content, diags := file.Body.Content(&hcl.BodySchema{
  1277  					Blocks: []hcl.BlockHeaderSchema{{Type: "rule", LabelNames: []string{"name"}}},
  1278  				})
  1279  				if diags.HasErrors() {
  1280  					return &hclext.BodyContent{}, sources, diags
  1281  				}
  1282  
  1283  				body, diags := hclext.Content(content.Blocks[0].Body, schema)
  1284  				if diags.HasErrors() {
  1285  					return &hclext.BodyContent{}, sources, diags
  1286  				}
  1287  				return body, sources, nil
  1288  			},
  1289  			Want:     &ruleConfig{Name: "foo"},
  1290  			ErrCheck: neverHappend,
  1291  		},
  1292  		{
  1293  			Name:     "server returns an error",
  1294  			RuleName: "test_rule",
  1295  			Target:   &ruleConfig{},
  1296  			ServerImpl: func(name string, schema *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) {
  1297  				return nil, map[string][]byte{}, errors.New("unexpected error")
  1298  			},
  1299  			Want: &ruleConfig{},
  1300  			ErrCheck: func(err error) bool {
  1301  				return err == nil || err.Error() != "unexpected error"
  1302  			},
  1303  		},
  1304  		{
  1305  			Name:     "response body is empty",
  1306  			RuleName: "test_rule",
  1307  			Target:   &ruleConfig{},
  1308  			ServerImpl: func(name string, schema *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) {
  1309  				return nil, map[string][]byte{}, nil
  1310  			},
  1311  			Want: &ruleConfig{},
  1312  			ErrCheck: func(err error) bool {
  1313  				return err == nil || err.Error() != "response body is empty"
  1314  			},
  1315  		},
  1316  		{
  1317  			Name:     "config not found",
  1318  			RuleName: "not_found",
  1319  			Target:   &ruleConfig{},
  1320  			ServerImpl: func(name string, schema *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) {
  1321  				return &hclext.BodyContent{}, nil, nil
  1322  			},
  1323  			Want:     &ruleConfig{},
  1324  			ErrCheck: neverHappend,
  1325  		},
  1326  		{
  1327  			Name:     "config not found with non-empty config",
  1328  			RuleName: "not_found",
  1329  			Target:   &ruleConfig{},
  1330  			ServerImpl: func(name string, schema *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) {
  1331  				return &hclext.BodyContent{Attributes: hclext.Attributes{"foo": &hclext.Attribute{}}}, nil, nil
  1332  			},
  1333  			Want: &ruleConfig{},
  1334  			ErrCheck: func(err error) bool {
  1335  				return err == nil || err.Error() != "config file not found"
  1336  			},
  1337  		},
  1338  		{
  1339  			Name:     "name is empty",
  1340  			RuleName: "",
  1341  			Target:   &ruleConfig{},
  1342  			Want:     &ruleConfig{},
  1343  			ErrCheck: func(err error) bool {
  1344  				return err == nil || err.Error() != "name should not be empty"
  1345  			},
  1346  		},
  1347  	}
  1348  
  1349  	for _, test := range tests {
  1350  		t.Run(test.Name, func(t *testing.T) {
  1351  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{getRuleConfigContent: test.ServerImpl}))
  1352  
  1353  			err := client.DecodeRuleConfig(test.RuleName, test.Target)
  1354  			if test.ErrCheck(err) {
  1355  				t.Fatalf("failed to call DecodeRuleConfig: %s", err)
  1356  			}
  1357  
  1358  			if diff := cmp.Diff(test.Target, test.Want); diff != "" {
  1359  				t.Errorf("diff: %s", diff)
  1360  			}
  1361  		})
  1362  	}
  1363  }
  1364  
  1365  func TestEvaluateExpr(t *testing.T) {
  1366  	// default error check helper
  1367  	neverHappend := func(err error) bool { return err != nil }
  1368  
  1369  	// default getFileImpl function
  1370  	fileIdx := 1
  1371  	files := map[string]*hcl.File{}
  1372  	fileExists := func(filename string) (*hcl.File, error) {
  1373  		return files[filename], nil
  1374  	}
  1375  
  1376  	// test util functions
  1377  	hclExpr := func(expr string) hcl.Expression {
  1378  		filename := fmt.Sprintf("test%d.tf", fileIdx)
  1379  		file, diags := hclsyntax.ParseConfig([]byte(fmt.Sprintf(`expr = %s`, expr)), filename, hcl.InitialPos)
  1380  		if diags.HasErrors() {
  1381  			panic(diags)
  1382  		}
  1383  		attributes, diags := file.Body.JustAttributes()
  1384  		if diags.HasErrors() {
  1385  			panic(diags)
  1386  		}
  1387  		files[filename] = file
  1388  		fileIdx = fileIdx + 1
  1389  		return attributes["expr"].Expr
  1390  	}
  1391  	jsonExpr := func(expr string) hcl.Expression {
  1392  		filename := fmt.Sprintf("test%d.tf.json", fileIdx)
  1393  		file, diags := json.Parse([]byte(fmt.Sprintf(`{"expr": %s}`, expr)), filename)
  1394  		if diags.HasErrors() {
  1395  			panic(diags)
  1396  		}
  1397  		attributes, diags := file.Body.JustAttributes()
  1398  		if diags.HasErrors() {
  1399  			panic(diags)
  1400  		}
  1401  		files[filename] = file
  1402  		fileIdx = fileIdx + 1
  1403  		return attributes["expr"].Expr
  1404  	}
  1405  	evalExpr := func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, error) {
  1406  		val, diags := expr.Value(ctx)
  1407  		if diags.HasErrors() {
  1408  			return cty.Value{}, diags
  1409  		}
  1410  		return val, nil
  1411  	}
  1412  
  1413  	// test struct for decoding from cty.Value
  1414  	type Object struct {
  1415  		Name    string `cty:"name"`
  1416  		Enabled bool   `cty:"enabled"`
  1417  	}
  1418  	objectTy := cty.Object(map[string]cty.Type{"name": cty.String, "enabled": cty.Bool})
  1419  
  1420  	tests := []struct {
  1421  		Name        string
  1422  		Expr        hcl.Expression
  1423  		TargetType  reflect.Type
  1424  		Option      *tflint.EvaluateExprOption
  1425  		ServerImpl  func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error)
  1426  		GetFileImpl func(string) (*hcl.File, error)
  1427  		Want        interface{}
  1428  		ErrCheck    func(err error) bool
  1429  	}{
  1430  		{
  1431  			Name:       "literal",
  1432  			Expr:       hclExpr(`"foo"`),
  1433  			TargetType: reflect.TypeOf(""),
  1434  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1435  				if *opts.WantType != cty.String {
  1436  					return cty.Value{}, errors.New("wantType should be string")
  1437  				}
  1438  				if opts.ModuleCtx != tflint.SelfModuleCtxType {
  1439  					return cty.Value{}, errors.New("moduleCtx should be self")
  1440  				}
  1441  				return evalExpr(expr, nil)
  1442  			},
  1443  			Want:        "foo",
  1444  			GetFileImpl: fileExists,
  1445  			ErrCheck:    neverHappend,
  1446  		},
  1447  		{
  1448  			Name:       "string variable",
  1449  			Expr:       hclExpr(`var.foo`),
  1450  			TargetType: reflect.TypeOf(""),
  1451  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1452  				return evalExpr(expr, &hcl.EvalContext{
  1453  					Variables: map[string]cty.Value{
  1454  						"var": cty.MapVal(map[string]cty.Value{
  1455  							"foo": cty.StringVal("bar"),
  1456  						}),
  1457  					},
  1458  				})
  1459  			},
  1460  			Want:        "bar",
  1461  			GetFileImpl: fileExists,
  1462  			ErrCheck:    neverHappend,
  1463  		},
  1464  		{
  1465  			Name:       "number variable",
  1466  			Expr:       hclExpr(`var.foo`),
  1467  			TargetType: reflect.TypeOf(0),
  1468  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1469  				if *opts.WantType != cty.Number {
  1470  					return cty.Value{}, errors.New("wantType should be number")
  1471  				}
  1472  				return evalExpr(expr, &hcl.EvalContext{
  1473  					Variables: map[string]cty.Value{
  1474  						"var": cty.MapVal(map[string]cty.Value{
  1475  							"foo": cty.NumberIntVal(7),
  1476  						}),
  1477  					},
  1478  				})
  1479  			},
  1480  			Want:        7,
  1481  			GetFileImpl: fileExists,
  1482  			ErrCheck:    neverHappend,
  1483  		},
  1484  		{
  1485  			Name:       "bool variable",
  1486  			Expr:       hclExpr(`var.foo`),
  1487  			TargetType: reflect.TypeOf(true),
  1488  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1489  				if *opts.WantType != cty.Bool {
  1490  					return cty.Value{}, errors.New("wantType should be bool")
  1491  				}
  1492  				return evalExpr(expr, &hcl.EvalContext{
  1493  					Variables: map[string]cty.Value{
  1494  						"var": cty.MapVal(map[string]cty.Value{
  1495  							"foo": cty.BoolVal(true),
  1496  						}),
  1497  					},
  1498  				})
  1499  			},
  1500  			Want:        true,
  1501  			GetFileImpl: fileExists,
  1502  			ErrCheck:    neverHappend,
  1503  		},
  1504  		{
  1505  			Name:       "string list variable",
  1506  			Expr:       hclExpr(`var.foo`),
  1507  			TargetType: reflect.TypeOf([]string{}),
  1508  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1509  				if *opts.WantType != cty.List(cty.String) {
  1510  					return cty.Value{}, errors.New("wantType should be string list")
  1511  				}
  1512  				return evalExpr(expr, &hcl.EvalContext{
  1513  					Variables: map[string]cty.Value{
  1514  						"var": cty.MapVal(map[string]cty.Value{
  1515  							"foo": cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}),
  1516  						}),
  1517  					},
  1518  				})
  1519  			},
  1520  			Want:        []string{"foo", "bar"},
  1521  			GetFileImpl: fileExists,
  1522  			ErrCheck:    neverHappend,
  1523  		},
  1524  		{
  1525  			Name:       "number list variable",
  1526  			Expr:       hclExpr(`var.foo`),
  1527  			TargetType: reflect.TypeOf([]int{}),
  1528  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1529  				if *opts.WantType != cty.List(cty.Number) {
  1530  					return cty.Value{}, errors.New("wantType should be number list")
  1531  				}
  1532  				return evalExpr(expr, &hcl.EvalContext{
  1533  					Variables: map[string]cty.Value{
  1534  						"var": cty.MapVal(map[string]cty.Value{
  1535  							"foo": cty.ListVal([]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2)}),
  1536  						}),
  1537  					},
  1538  				})
  1539  			},
  1540  			Want:        []int{1, 2},
  1541  			GetFileImpl: fileExists,
  1542  			ErrCheck:    neverHappend,
  1543  		},
  1544  		{
  1545  			Name:       "bool list variable",
  1546  			Expr:       hclExpr(`var.foo`),
  1547  			TargetType: reflect.TypeOf([]bool{}),
  1548  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1549  				if *opts.WantType != cty.List(cty.Bool) {
  1550  					return cty.Value{}, errors.New("wantType should be bool list")
  1551  				}
  1552  				return evalExpr(expr, &hcl.EvalContext{
  1553  					Variables: map[string]cty.Value{
  1554  						"var": cty.MapVal(map[string]cty.Value{
  1555  							"foo": cty.ListVal([]cty.Value{cty.BoolVal(true), cty.BoolVal(false)}),
  1556  						}),
  1557  					},
  1558  				})
  1559  			},
  1560  			Want:        []bool{true, false},
  1561  			GetFileImpl: fileExists,
  1562  			ErrCheck:    neverHappend,
  1563  		},
  1564  		{
  1565  			Name:       "string map variable",
  1566  			Expr:       hclExpr(`var.foo`),
  1567  			TargetType: reflect.TypeOf(map[string]string{}),
  1568  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1569  				if *opts.WantType != cty.Map(cty.String) {
  1570  					return cty.Value{}, errors.New("wantType should be string map")
  1571  				}
  1572  				return evalExpr(expr, &hcl.EvalContext{
  1573  					Variables: map[string]cty.Value{
  1574  						"var": cty.MapVal(map[string]cty.Value{
  1575  							"foo": cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("bar"), "baz": cty.StringVal("qux")}),
  1576  						}),
  1577  					},
  1578  				})
  1579  			},
  1580  			Want:        map[string]string{"foo": "bar", "baz": "qux"},
  1581  			GetFileImpl: fileExists,
  1582  			ErrCheck:    neverHappend,
  1583  		},
  1584  		{
  1585  			Name:       "number map variable",
  1586  			Expr:       hclExpr(`var.foo`),
  1587  			TargetType: reflect.TypeOf(map[string]int{}),
  1588  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1589  				if *opts.WantType != cty.Map(cty.Number) {
  1590  					return cty.Value{}, errors.New("wantType should be number map")
  1591  				}
  1592  				return evalExpr(expr, &hcl.EvalContext{
  1593  					Variables: map[string]cty.Value{
  1594  						"var": cty.MapVal(map[string]cty.Value{
  1595  							"foo": cty.MapVal(map[string]cty.Value{"foo": cty.NumberIntVal(1), "bar": cty.NumberIntVal(2)}),
  1596  						}),
  1597  					},
  1598  				})
  1599  			},
  1600  			Want:        map[string]int{"foo": 1, "bar": 2},
  1601  			GetFileImpl: fileExists,
  1602  			ErrCheck:    neverHappend,
  1603  		},
  1604  		{
  1605  			Name:       "bool map variable",
  1606  			Expr:       hclExpr(`var.foo`),
  1607  			TargetType: reflect.TypeOf(map[string]bool{}),
  1608  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1609  				if *opts.WantType != cty.Map(cty.Bool) {
  1610  					return cty.Value{}, errors.New("wantType should be bool map")
  1611  				}
  1612  				return evalExpr(expr, &hcl.EvalContext{
  1613  					Variables: map[string]cty.Value{
  1614  						"var": cty.MapVal(map[string]cty.Value{
  1615  							"foo": cty.MapVal(map[string]cty.Value{"foo": cty.BoolVal(true), "bar": cty.BoolVal(false)}),
  1616  						}),
  1617  					},
  1618  				})
  1619  			},
  1620  			Want:        map[string]bool{"foo": true, "bar": false},
  1621  			GetFileImpl: fileExists,
  1622  			ErrCheck:    neverHappend,
  1623  		},
  1624  		{
  1625  			Name:       "object variable",
  1626  			Expr:       hclExpr(`var.foo`),
  1627  			TargetType: reflect.TypeOf(cty.Value{}),
  1628  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1629  				if *opts.WantType != cty.DynamicPseudoType {
  1630  					return cty.Value{}, errors.New("wantType should be pseudo type")
  1631  				}
  1632  				return evalExpr(expr, &hcl.EvalContext{
  1633  					Variables: map[string]cty.Value{
  1634  						"var": cty.MapVal(map[string]cty.Value{
  1635  							"foo": cty.ObjectVal(map[string]cty.Value{
  1636  								"foo": cty.NumberIntVal(1),
  1637  								"bar": cty.StringVal("baz"),
  1638  								"qux": cty.UnknownVal(cty.String),
  1639  							}),
  1640  						}),
  1641  					},
  1642  				})
  1643  			},
  1644  			Want: cty.ObjectVal(map[string]cty.Value{
  1645  				"foo": cty.NumberIntVal(1),
  1646  				"bar": cty.StringVal("baz"),
  1647  				"qux": cty.UnknownVal(cty.String),
  1648  			}),
  1649  			GetFileImpl: fileExists,
  1650  			ErrCheck:    neverHappend,
  1651  		},
  1652  		{
  1653  			Name:       "object variable to struct",
  1654  			Expr:       hclExpr(`var.foo`),
  1655  			TargetType: reflect.TypeOf(Object{}),
  1656  			Option:     &tflint.EvaluateExprOption{WantType: &objectTy},
  1657  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1658  				return evalExpr(expr, &hcl.EvalContext{
  1659  					Variables: map[string]cty.Value{
  1660  						"var": cty.MapVal(map[string]cty.Value{
  1661  							"foo": cty.ObjectVal(map[string]cty.Value{
  1662  								"name":    cty.StringVal("bar"),
  1663  								"enabled": cty.BoolVal(true),
  1664  							}),
  1665  						}),
  1666  					},
  1667  				})
  1668  			},
  1669  			Want:        Object{Name: "bar", Enabled: true},
  1670  			GetFileImpl: fileExists,
  1671  			ErrCheck:    neverHappend,
  1672  		},
  1673  		{
  1674  			Name:       "JSON expr",
  1675  			Expr:       jsonExpr(`"${var.foo}"`),
  1676  			TargetType: reflect.TypeOf(""),
  1677  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1678  				return evalExpr(expr, &hcl.EvalContext{
  1679  					Variables: map[string]cty.Value{
  1680  						"var": cty.MapVal(map[string]cty.Value{
  1681  							"foo": cty.StringVal("bar"),
  1682  						}),
  1683  					},
  1684  				})
  1685  			},
  1686  			Want:        "bar",
  1687  			GetFileImpl: fileExists,
  1688  			ErrCheck:    neverHappend,
  1689  		},
  1690  		{
  1691  			Name:       "JSON object",
  1692  			Expr:       jsonExpr(`{"foo": "bar"}`),
  1693  			TargetType: reflect.TypeOf(map[string]string{}),
  1694  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1695  				return evalExpr(expr, nil)
  1696  			},
  1697  			Want:        map[string]string{"foo": "bar"},
  1698  			GetFileImpl: fileExists,
  1699  			ErrCheck:    neverHappend,
  1700  		},
  1701  		{
  1702  			Name:       "bound expr",
  1703  			Expr:       hclext.BindValue(cty.StringVal("bound value"), hclExpr(`var.foo`)),
  1704  			TargetType: reflect.TypeOf(""),
  1705  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1706  				return evalExpr(expr, &hcl.EvalContext{})
  1707  			},
  1708  			Want:        "bound value",
  1709  			GetFileImpl: fileExists,
  1710  			ErrCheck:    neverHappend,
  1711  		},
  1712  		{
  1713  			Name:       "eval with moduleCtx option",
  1714  			Expr:       hclExpr(`1`),
  1715  			TargetType: reflect.TypeOf(0),
  1716  			Option:     &tflint.EvaluateExprOption{ModuleCtx: tflint.RootModuleCtxType},
  1717  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1718  				if opts.ModuleCtx != tflint.RootModuleCtxType {
  1719  					return cty.Value{}, errors.New("moduleCtx should be root")
  1720  				}
  1721  				return evalExpr(expr, nil)
  1722  			},
  1723  			Want:        1,
  1724  			GetFileImpl: fileExists,
  1725  			ErrCheck:    neverHappend,
  1726  		},
  1727  		{
  1728  			Name:       "getFile returns no file",
  1729  			Expr:       hclExpr(`1`),
  1730  			TargetType: reflect.TypeOf(0),
  1731  			Want:       0,
  1732  			GetFileImpl: func(string) (*hcl.File, error) {
  1733  				return nil, nil
  1734  			},
  1735  			ErrCheck: func(err error) bool {
  1736  				return err == nil || err.Error() != "file not found"
  1737  			},
  1738  		},
  1739  		{
  1740  			Name:       "getFile returns an error",
  1741  			Expr:       hclExpr(`1`),
  1742  			TargetType: reflect.TypeOf(0),
  1743  			Want:       0,
  1744  			GetFileImpl: func(string) (*hcl.File, error) {
  1745  				return nil, errors.New("unexpected error")
  1746  			},
  1747  			ErrCheck: func(err error) bool {
  1748  				return err == nil || err.Error() != "unexpected error"
  1749  			},
  1750  		},
  1751  		{
  1752  			Name:       "server returns an unexpected error",
  1753  			Expr:       hclExpr(`1`),
  1754  			TargetType: reflect.TypeOf(0),
  1755  			ServerImpl: func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) {
  1756  				return cty.Value{}, errors.New("unexpected error")
  1757  			},
  1758  			Want:        0,
  1759  			GetFileImpl: fileExists,
  1760  			ErrCheck: func(err error) bool {
  1761  				return err == nil || err.Error() != "unexpected error"
  1762  			},
  1763  		},
  1764  		{
  1765  			Name:       "server returns an unknown error",
  1766  			Expr:       hclExpr(`1`),
  1767  			TargetType: reflect.TypeOf(0),
  1768  			ServerImpl: func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) {
  1769  				return cty.Value{}, fmt.Errorf("unknown%w", tflint.ErrUnknownValue)
  1770  			},
  1771  			Want:        0,
  1772  			GetFileImpl: fileExists,
  1773  			ErrCheck: func(err error) bool {
  1774  				return !errors.Is(err, tflint.ErrUnknownValue)
  1775  			},
  1776  		},
  1777  		{
  1778  			Name:       "server returns a null value error",
  1779  			Expr:       hclExpr(`1`),
  1780  			TargetType: reflect.TypeOf(0),
  1781  			ServerImpl: func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) {
  1782  				return cty.Value{}, fmt.Errorf("null value%w", tflint.ErrNullValue)
  1783  			},
  1784  			Want:        0,
  1785  			GetFileImpl: fileExists,
  1786  			ErrCheck: func(err error) bool {
  1787  				return !errors.Is(err, tflint.ErrNullValue)
  1788  			},
  1789  		},
  1790  		{
  1791  			Name:       "server returns a unevaluable error",
  1792  			Expr:       hclExpr(`1`),
  1793  			TargetType: reflect.TypeOf(0),
  1794  			ServerImpl: func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) {
  1795  				return cty.Value{}, fmt.Errorf("unevaluable%w", tflint.ErrUnevaluable)
  1796  			},
  1797  			Want:        0,
  1798  			GetFileImpl: fileExists,
  1799  			ErrCheck: func(err error) bool {
  1800  				return !errors.Is(err, tflint.ErrUnevaluable)
  1801  			},
  1802  		},
  1803  		{
  1804  			Name:       "server returns a sensitive error",
  1805  			Expr:       hclExpr(`1`),
  1806  			TargetType: reflect.TypeOf(0),
  1807  			ServerImpl: func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) {
  1808  				return cty.Value{}, fmt.Errorf("sensitive%w", tflint.ErrSensitive)
  1809  			},
  1810  			Want:        0,
  1811  			GetFileImpl: fileExists,
  1812  			ErrCheck: func(err error) bool {
  1813  				return !errors.Is(err, tflint.ErrSensitive)
  1814  			},
  1815  		},
  1816  		{
  1817  			Name:       "unknown value",
  1818  			Expr:       hclExpr(`var.foo`),
  1819  			TargetType: reflect.TypeOf(""),
  1820  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1821  				return evalExpr(expr, &hcl.EvalContext{
  1822  					Variables: map[string]cty.Value{
  1823  						"var": cty.MapVal(map[string]cty.Value{
  1824  							"foo": cty.UnknownVal(cty.String),
  1825  						}),
  1826  					},
  1827  				})
  1828  			},
  1829  			Want:        "",
  1830  			GetFileImpl: fileExists,
  1831  			ErrCheck: func(err error) bool {
  1832  				return !errors.Is(err, tflint.ErrUnknownValue)
  1833  			},
  1834  		},
  1835  		{
  1836  			Name:       "unknown value as cty.Value",
  1837  			Expr:       hclExpr(`var.foo`),
  1838  			TargetType: reflect.TypeOf(cty.Value{}),
  1839  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1840  				return evalExpr(expr, &hcl.EvalContext{
  1841  					Variables: map[string]cty.Value{
  1842  						"var": cty.MapVal(map[string]cty.Value{
  1843  							"foo": cty.UnknownVal(cty.String),
  1844  						}),
  1845  					},
  1846  				})
  1847  			},
  1848  			Want:        cty.UnknownVal(cty.String),
  1849  			GetFileImpl: fileExists,
  1850  			ErrCheck:    neverHappend,
  1851  		},
  1852  		{
  1853  			Name:       "unknown value in object",
  1854  			Expr:       hclExpr(`{ value = var.foo }`),
  1855  			TargetType: reflect.TypeOf(map[string]string{}),
  1856  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1857  				return evalExpr(expr, &hcl.EvalContext{
  1858  					Variables: map[string]cty.Value{
  1859  						"var": cty.MapVal(map[string]cty.Value{
  1860  							"foo": cty.UnknownVal(cty.String),
  1861  						}),
  1862  					},
  1863  				})
  1864  			},
  1865  			Want:        (map[string]string)(nil),
  1866  			GetFileImpl: fileExists,
  1867  			ErrCheck: func(err error) bool {
  1868  				return !errors.Is(err, tflint.ErrUnknownValue)
  1869  			},
  1870  		},
  1871  		{
  1872  			Name:       "null",
  1873  			Expr:       hclExpr(`var.foo`),
  1874  			TargetType: reflect.TypeOf(""),
  1875  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1876  				return evalExpr(expr, &hcl.EvalContext{
  1877  					Variables: map[string]cty.Value{
  1878  						"var": cty.MapVal(map[string]cty.Value{
  1879  							"foo": cty.NullVal(cty.String),
  1880  						}),
  1881  					},
  1882  				})
  1883  			},
  1884  			Want:        "",
  1885  			GetFileImpl: fileExists,
  1886  			ErrCheck: func(err error) bool {
  1887  				return !errors.Is(err, tflint.ErrNullValue)
  1888  			},
  1889  		},
  1890  		{
  1891  			Name:       "null as cty.Value",
  1892  			Expr:       hclExpr(`var.foo`),
  1893  			TargetType: reflect.TypeOf(cty.Value{}),
  1894  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1895  				return evalExpr(expr, &hcl.EvalContext{
  1896  					Variables: map[string]cty.Value{
  1897  						"var": cty.MapVal(map[string]cty.Value{
  1898  							"foo": cty.NullVal(cty.String),
  1899  						}),
  1900  					},
  1901  				})
  1902  			},
  1903  			Want:        cty.NullVal(cty.String),
  1904  			GetFileImpl: fileExists,
  1905  			ErrCheck:    neverHappend,
  1906  		},
  1907  		{
  1908  			Name:       "null value in object",
  1909  			Expr:       hclExpr(`{ value = var.foo }`),
  1910  			TargetType: reflect.TypeOf(map[string]string{}),
  1911  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1912  				return evalExpr(expr, &hcl.EvalContext{
  1913  					Variables: map[string]cty.Value{
  1914  						"var": cty.MapVal(map[string]cty.Value{
  1915  							"foo": cty.NullVal(cty.String),
  1916  						}),
  1917  					},
  1918  				})
  1919  			},
  1920  			Want:        (map[string]string)(nil),
  1921  			GetFileImpl: fileExists,
  1922  			ErrCheck: func(err error) bool {
  1923  				return !errors.Is(err, tflint.ErrNullValue)
  1924  			},
  1925  		},
  1926  		{
  1927  			Name:       "sensitive",
  1928  			Expr:       hclExpr(`var.foo`),
  1929  			TargetType: reflect.TypeOf(""),
  1930  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1931  				return evalExpr(expr, &hcl.EvalContext{
  1932  					Variables: map[string]cty.Value{
  1933  						"var": cty.MapVal(map[string]cty.Value{
  1934  							"foo": cty.StringVal("bar").Mark(marks.Sensitive),
  1935  						}),
  1936  					},
  1937  				})
  1938  			},
  1939  			Want:        "",
  1940  			GetFileImpl: fileExists,
  1941  			ErrCheck: func(err error) bool {
  1942  				return !errors.Is(err, tflint.ErrSensitive)
  1943  			},
  1944  		},
  1945  		{
  1946  			Name:       "sensitive as cty.Value",
  1947  			Expr:       hclExpr(`var.foo`),
  1948  			TargetType: reflect.TypeOf(cty.Value{}),
  1949  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1950  				return evalExpr(expr, &hcl.EvalContext{
  1951  					Variables: map[string]cty.Value{
  1952  						"var": cty.MapVal(map[string]cty.Value{
  1953  							"foo": cty.StringVal("bar").Mark(marks.Sensitive),
  1954  						}),
  1955  					},
  1956  				})
  1957  			},
  1958  			Want:        cty.StringVal("bar").Mark(marks.Sensitive),
  1959  			GetFileImpl: fileExists,
  1960  			ErrCheck:    neverHappend,
  1961  		},
  1962  		{
  1963  			Name:       "sensitive in object",
  1964  			Expr:       hclExpr(`{ value = var.foo }`),
  1965  			TargetType: reflect.TypeOf(map[string]string{}),
  1966  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1967  				return evalExpr(expr, &hcl.EvalContext{
  1968  					Variables: map[string]cty.Value{
  1969  						"var": cty.MapVal(map[string]cty.Value{
  1970  							"foo": cty.StringVal("bar").Mark(marks.Sensitive),
  1971  						}),
  1972  					},
  1973  				})
  1974  			},
  1975  			Want:        (map[string]string)(nil),
  1976  			GetFileImpl: fileExists,
  1977  			ErrCheck: func(err error) bool {
  1978  				return !errors.Is(err, tflint.ErrSensitive)
  1979  			},
  1980  		},
  1981  		{
  1982  			Name:       "sensitive object as cty.Value",
  1983  			Expr:       hclExpr(`var.foo`),
  1984  			TargetType: reflect.TypeOf(cty.Value{}),
  1985  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  1986  				return evalExpr(expr, &hcl.EvalContext{
  1987  					Variables: map[string]cty.Value{
  1988  						"var": cty.MapVal(map[string]cty.Value{
  1989  							"foo": cty.ObjectVal(map[string]cty.Value{
  1990  								"bar": cty.StringVal("barval").Mark(marks.Sensitive),
  1991  								"baz": cty.ListVal([]cty.Value{cty.NumberIntVal(1).Mark(marks.Sensitive)}),
  1992  								"qux": cty.TupleVal([]cty.Value{cty.StringVal("quxval").Mark(marks.Sensitive)}),
  1993  								"quux": cty.MapVal(map[string]cty.Value{
  1994  									"foo": cty.StringVal("fooval").Mark(marks.Sensitive),
  1995  								}),
  1996  								"corge": cty.ObjectVal(map[string]cty.Value{
  1997  									"bar": cty.NumberIntVal(2).Mark(marks.Sensitive),
  1998  								}),
  1999  							}),
  2000  						}),
  2001  					},
  2002  				})
  2003  			},
  2004  			Want: cty.ObjectVal(map[string]cty.Value{
  2005  				"bar": cty.StringVal("barval").Mark(marks.Sensitive),
  2006  				"baz": cty.ListVal([]cty.Value{cty.NumberIntVal(1).Mark(marks.Sensitive)}),
  2007  				"qux": cty.TupleVal([]cty.Value{cty.StringVal("quxval").Mark(marks.Sensitive)}),
  2008  				"quux": cty.MapVal(map[string]cty.Value{
  2009  					"foo": cty.StringVal("fooval").Mark(marks.Sensitive),
  2010  				}),
  2011  				"corge": cty.ObjectVal(map[string]cty.Value{
  2012  					"bar": cty.NumberIntVal(2).Mark(marks.Sensitive),
  2013  				}),
  2014  			}),
  2015  			GetFileImpl: fileExists,
  2016  			ErrCheck:    neverHappend,
  2017  		},
  2018  		{
  2019  			Name:       "ephemeral",
  2020  			Expr:       hclExpr(`var.foo`),
  2021  			TargetType: reflect.TypeOf(""),
  2022  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2023  				return evalExpr(expr, &hcl.EvalContext{
  2024  					Variables: map[string]cty.Value{
  2025  						"var": cty.MapVal(map[string]cty.Value{
  2026  							"foo": cty.StringVal("bar").Mark(marks.Ephemeral),
  2027  						}),
  2028  					},
  2029  				})
  2030  			},
  2031  			Want:        "",
  2032  			GetFileImpl: fileExists,
  2033  			ErrCheck: func(err error) bool {
  2034  				return !errors.Is(err, tflint.ErrEphemeral)
  2035  			},
  2036  		},
  2037  		{
  2038  			Name:       "ephemeral as cty.Value",
  2039  			Expr:       hclExpr(`var.foo`),
  2040  			TargetType: reflect.TypeOf(cty.Value{}),
  2041  			ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2042  				return evalExpr(expr, &hcl.EvalContext{
  2043  					Variables: map[string]cty.Value{
  2044  						"var": cty.MapVal(map[string]cty.Value{
  2045  							"foo": cty.StringVal("bar").Mark(marks.Ephemeral),
  2046  						}),
  2047  					},
  2048  				})
  2049  			},
  2050  			Want:        cty.StringVal("bar").Mark(marks.Ephemeral),
  2051  			GetFileImpl: fileExists,
  2052  			ErrCheck:    neverHappend,
  2053  		},
  2054  	}
  2055  
  2056  	for _, test := range tests {
  2057  		t.Run(test.Name, func(t *testing.T) {
  2058  			target := reflect.New(test.TargetType)
  2059  
  2060  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{evaluateExpr: test.ServerImpl, getFile: test.GetFileImpl}))
  2061  
  2062  			err := client.EvaluateExpr(test.Expr, target.Interface(), test.Option)
  2063  			if test.ErrCheck(err) {
  2064  				t.Fatalf("failed to call EvaluateExpr: %s", err)
  2065  			}
  2066  
  2067  			got := target.Elem().Interface()
  2068  
  2069  			opts := cmp.Options{
  2070  				cmp.Comparer(func(x, y cty.Value) bool {
  2071  					return x.GoString() == y.GoString()
  2072  				}),
  2073  			}
  2074  			if diff := cmp.Diff(got, test.Want, opts); diff != "" {
  2075  				t.Errorf("diff: %s", diff)
  2076  			}
  2077  		})
  2078  	}
  2079  }
  2080  
  2081  func TestEvaluateExpr_callback(t *testing.T) {
  2082  	// default error check helper
  2083  	neverHappend := func(err error) bool { return err != nil }
  2084  
  2085  	// default getFileImpl function
  2086  	fileIdx := 1
  2087  	files := map[string]*hcl.File{}
  2088  	fileExists := func(filename string) (*hcl.File, error) {
  2089  		return files[filename], nil
  2090  	}
  2091  
  2092  	// test util functions
  2093  	hclExpr := func(expr string) hcl.Expression {
  2094  		filename := fmt.Sprintf("test%d.tf", fileIdx)
  2095  		file, diags := hclsyntax.ParseConfig([]byte(fmt.Sprintf(`expr = %s`, expr)), filename, hcl.InitialPos)
  2096  		if diags.HasErrors() {
  2097  			panic(diags)
  2098  		}
  2099  		attributes, diags := file.Body.JustAttributes()
  2100  		if diags.HasErrors() {
  2101  			panic(diags)
  2102  		}
  2103  		files[filename] = file
  2104  		fileIdx = fileIdx + 1
  2105  		return attributes["expr"].Expr
  2106  	}
  2107  
  2108  	// test struct for decoding from cty.Value
  2109  	type Object struct {
  2110  		Name    string `cty:"name"`
  2111  		Enabled bool   `cty:"enabled"`
  2112  	}
  2113  	objectTy := cty.Object(map[string]cty.Type{"name": cty.String, "enabled": cty.Bool})
  2114  
  2115  	tests := []struct {
  2116  		name        string
  2117  		expr        hcl.Expression
  2118  		target      any
  2119  		option      *tflint.EvaluateExprOption
  2120  		serverImpl  func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error)
  2121  		getFileImpl func(string) (*hcl.File, error)
  2122  		errCheck    func(err error) bool
  2123  	}{
  2124  		{
  2125  			name: "callback with string",
  2126  			expr: hclExpr(`"foo"`),
  2127  			target: func(val string) error {
  2128  				return fmt.Errorf("value is %s", val)
  2129  			},
  2130  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2131  				return cty.StringVal("foo"), nil
  2132  			},
  2133  			getFileImpl: fileExists,
  2134  			errCheck: func(err error) bool {
  2135  				return err == nil || err.Error() != "value is foo"
  2136  			},
  2137  		},
  2138  		{
  2139  			name: "callback with int",
  2140  			expr: hclExpr(`1`),
  2141  			target: func(val int) error {
  2142  				return fmt.Errorf("value is %d", val)
  2143  			},
  2144  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2145  				return cty.NumberIntVal(1), nil
  2146  			},
  2147  			getFileImpl: fileExists,
  2148  			errCheck: func(err error) bool {
  2149  				return err == nil || err.Error() != "value is 1"
  2150  			},
  2151  		},
  2152  		{
  2153  			name: "callback with bool",
  2154  			expr: hclExpr(`true`),
  2155  			target: func(val bool) error {
  2156  				return fmt.Errorf("value is %t", val)
  2157  			},
  2158  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2159  				return cty.BoolVal(true), nil
  2160  			},
  2161  			getFileImpl: fileExists,
  2162  			errCheck: func(err error) bool {
  2163  				return err == nil || err.Error() != "value is true"
  2164  			},
  2165  		},
  2166  		{
  2167  			name: "callback with []string",
  2168  			expr: hclExpr(`["foo", "bar"]`),
  2169  			target: func(val []string) error {
  2170  				return fmt.Errorf("value is %#v", val)
  2171  			},
  2172  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2173  				return cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), nil
  2174  			},
  2175  			getFileImpl: fileExists,
  2176  			errCheck: func(err error) bool {
  2177  				return err == nil || err.Error() != `value is []string{"foo", "bar"}`
  2178  			},
  2179  		},
  2180  		{
  2181  			name: "callback with []int",
  2182  			expr: hclExpr(`[1, 2]`),
  2183  			target: func(val []int) error {
  2184  				return fmt.Errorf("value is %#v", val)
  2185  			},
  2186  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2187  				return cty.ListVal([]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2)}), nil
  2188  			},
  2189  			getFileImpl: fileExists,
  2190  			errCheck: func(err error) bool {
  2191  				return err == nil || err.Error() != `value is []int{1, 2}`
  2192  			},
  2193  		},
  2194  		{
  2195  			name: "callback with []bool",
  2196  			expr: hclExpr(`[true, false]`),
  2197  			target: func(val []bool) error {
  2198  				return fmt.Errorf("value is %#v", val)
  2199  			},
  2200  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2201  				return cty.ListVal([]cty.Value{cty.BoolVal(true), cty.BoolVal(false)}), nil
  2202  			},
  2203  			getFileImpl: fileExists,
  2204  			errCheck: func(err error) bool {
  2205  				return err == nil || err.Error() != `value is []bool{true, false}`
  2206  			},
  2207  		},
  2208  		{
  2209  			name: "callback with map[string]string",
  2210  			expr: hclExpr(`{ "foo" = "bar", "baz" = "qux" }`),
  2211  			target: func(val map[string]string) error {
  2212  				return fmt.Errorf("foo is %s, baz is %s", val["foo"], val["baz"])
  2213  			},
  2214  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2215  				return cty.MapVal(map[string]cty.Value{
  2216  					"foo": cty.StringVal("bar"),
  2217  					"baz": cty.StringVal("qux"),
  2218  				}), nil
  2219  			},
  2220  			getFileImpl: fileExists,
  2221  			errCheck: func(err error) bool {
  2222  				return err == nil || err.Error() != `foo is bar, baz is qux`
  2223  			},
  2224  		},
  2225  		{
  2226  			name: "callback with map[string]int",
  2227  			expr: hclExpr(`{ "foo" = "bar", "baz" = "qux" }`),
  2228  			target: func(val map[string]int) error {
  2229  				return fmt.Errorf("foo is %d, baz is %d", val["foo"], val["baz"])
  2230  			},
  2231  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2232  				return cty.MapVal(map[string]cty.Value{
  2233  					"foo": cty.NumberIntVal(1),
  2234  					"baz": cty.NumberIntVal(2),
  2235  				}), nil
  2236  			},
  2237  			getFileImpl: fileExists,
  2238  			errCheck: func(err error) bool {
  2239  				return err == nil || err.Error() != `foo is 1, baz is 2`
  2240  			},
  2241  		},
  2242  		{
  2243  			name: "callback with map[string]bool",
  2244  			expr: hclExpr(`{ "foo" = true, "baz" = false }`),
  2245  			target: func(val map[string]bool) error {
  2246  				return fmt.Errorf("foo is %t, baz is %t", val["foo"], val["baz"])
  2247  			},
  2248  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2249  				return cty.MapVal(map[string]cty.Value{
  2250  					"foo": cty.BoolVal(true),
  2251  					"baz": cty.BoolVal(false),
  2252  				}), nil
  2253  			},
  2254  			getFileImpl: fileExists,
  2255  			errCheck: func(err error) bool {
  2256  				return err == nil || err.Error() != `foo is true, baz is false`
  2257  			},
  2258  		},
  2259  		{
  2260  			name: "callback with cty.Value",
  2261  			expr: hclExpr(`var.foo`),
  2262  			target: func(val cty.Value) error {
  2263  				return fmt.Errorf("value is %s", val.GoString())
  2264  			},
  2265  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2266  				return cty.ObjectVal(map[string]cty.Value{
  2267  					"foo": cty.NumberIntVal(1),
  2268  					"bar": cty.StringVal("baz"),
  2269  					"qux": cty.UnknownVal(cty.String),
  2270  				}), nil
  2271  			},
  2272  			getFileImpl: fileExists,
  2273  			errCheck: func(err error) bool {
  2274  				return err == nil || err.Error() != `value is cty.ObjectVal(map[string]cty.Value{"bar":cty.StringVal("baz"), "foo":cty.NumberIntVal(1), "qux":cty.UnknownVal(cty.String)})`
  2275  			},
  2276  		},
  2277  		{
  2278  			name: "callback with struct",
  2279  			expr: hclExpr(`var.foo`),
  2280  			target: func(val Object) error {
  2281  				return fmt.Errorf("value is %#v", val)
  2282  			},
  2283  			option: &tflint.EvaluateExprOption{WantType: &objectTy},
  2284  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2285  				return cty.ObjectVal(map[string]cty.Value{
  2286  					"name":    cty.StringVal("bar"),
  2287  					"enabled": cty.BoolVal(true),
  2288  				}), nil
  2289  			},
  2290  			getFileImpl: fileExists,
  2291  			errCheck: func(err error) bool {
  2292  				return err == nil || err.Error() != `value is plugin2host.Object{Name:"bar", Enabled:true}`
  2293  			},
  2294  		},
  2295  		{
  2296  			name: "callback with unknown value as Go value",
  2297  			expr: hclExpr(`var.foo`),
  2298  			target: func(val string) error {
  2299  				return fmt.Errorf("value is %s", val)
  2300  			},
  2301  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2302  				return cty.UnknownVal(cty.String), nil
  2303  			},
  2304  			getFileImpl: fileExists,
  2305  			errCheck:    neverHappend,
  2306  		},
  2307  		{
  2308  			name: "callback with unknown value as cty.Value",
  2309  			expr: hclExpr(`var.foo`),
  2310  			target: func(val cty.Value) error {
  2311  				return fmt.Errorf("value is %s", val.GoString())
  2312  			},
  2313  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2314  				return cty.UnknownVal(cty.String), nil
  2315  			},
  2316  			getFileImpl: fileExists,
  2317  			errCheck: func(err error) bool {
  2318  				return err == nil || err.Error() != `value is cty.UnknownVal(cty.String)`
  2319  			},
  2320  		},
  2321  		{
  2322  			name: "callback with null as Go value",
  2323  			expr: hclExpr(`var.foo`),
  2324  			target: func(val string) error {
  2325  				return fmt.Errorf("value is %s", val)
  2326  			},
  2327  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2328  				return cty.NullVal(cty.String), nil
  2329  			},
  2330  			getFileImpl: fileExists,
  2331  			errCheck:    neverHappend,
  2332  		},
  2333  		{
  2334  			name: "callback with null as cty.Value",
  2335  			expr: hclExpr(`var.foo`),
  2336  			target: func(val cty.Value) error {
  2337  				return fmt.Errorf("value is %s", val.GoString())
  2338  			},
  2339  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2340  				return cty.NullVal(cty.String), nil
  2341  			},
  2342  			getFileImpl: fileExists,
  2343  			errCheck: func(err error) bool {
  2344  				return err == nil || err.Error() != `value is cty.NullVal(cty.String)`
  2345  			},
  2346  		},
  2347  		{
  2348  			name: "callback with sensitive value as Go value",
  2349  			expr: hclExpr(`var.foo`),
  2350  			target: func(val string) error {
  2351  				return fmt.Errorf("value is %s", val)
  2352  			},
  2353  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2354  				return cty.StringVal("foo").Mark(marks.Sensitive), nil
  2355  			},
  2356  			getFileImpl: fileExists,
  2357  			errCheck:    neverHappend,
  2358  		},
  2359  		{
  2360  			name: "callback with sensitive value as cty.Value",
  2361  			expr: hclExpr(`var.foo`),
  2362  			target: func(val cty.Value) error {
  2363  				return fmt.Errorf("value is %s", val.GoString())
  2364  			},
  2365  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2366  				return cty.StringVal("foo").Mark(marks.Sensitive), nil
  2367  			},
  2368  			getFileImpl: fileExists,
  2369  			errCheck: func(err error) bool {
  2370  				return err == nil || err.Error() != `value is cty.StringVal("foo").Mark(marks.Sensitive)`
  2371  			},
  2372  		},
  2373  		{
  2374  			name: "callback with ephemeral value as Go value",
  2375  			expr: hclExpr(`var.foo`),
  2376  			target: func(val string) error {
  2377  				return fmt.Errorf("value is %s", val)
  2378  			},
  2379  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2380  				return cty.StringVal("foo").Mark(marks.Ephemeral), nil
  2381  			},
  2382  			getFileImpl: fileExists,
  2383  			errCheck:    neverHappend,
  2384  		},
  2385  		{
  2386  			name: "callback with ephemeral value as cty.Value",
  2387  			expr: hclExpr(`var.foo`),
  2388  			target: func(val cty.Value) error {
  2389  				return fmt.Errorf("value is %s", val.GoString())
  2390  			},
  2391  			serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
  2392  				return cty.StringVal("foo").Mark(marks.Ephemeral), nil
  2393  			},
  2394  			getFileImpl: fileExists,
  2395  			errCheck: func(err error) bool {
  2396  				return err == nil || err.Error() != `value is cty.StringVal("foo").Mark(marks.Ephemeral)`
  2397  			},
  2398  		},
  2399  	}
  2400  
  2401  	for _, test := range tests {
  2402  		t.Run(test.name, func(t *testing.T) {
  2403  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{evaluateExpr: test.serverImpl, getFile: test.getFileImpl}))
  2404  
  2405  			err := client.EvaluateExpr(test.expr, test.target, test.option)
  2406  			if test.errCheck(err) {
  2407  				t.Fatalf("failed to call EvaluateExpr: %s", err)
  2408  			}
  2409  		})
  2410  	}
  2411  }
  2412  
  2413  // test rule for TestEmitIssue
  2414  type Rule struct {
  2415  	tflint.DefaultRule
  2416  }
  2417  
  2418  func (*Rule) Name() string                     { return "test_rule" }
  2419  func (*Rule) Enabled() bool                    { return true }
  2420  func (*Rule) Severity() tflint.Severity        { return tflint.ERROR }
  2421  func (*Rule) Link() string                     { return "https://example.com" }
  2422  func (*Rule) Check(runner tflint.Runner) error { return nil }
  2423  
  2424  func TestEmitIssue(t *testing.T) {
  2425  	// default error check helper
  2426  	neverHappend := func(err error) bool { return err != nil }
  2427  
  2428  	tests := []struct {
  2429  		Name       string
  2430  		Args       func() (tflint.Rule, string, hcl.Range)
  2431  		ServerImpl func(tflint.Rule, string, hcl.Range, bool) (bool, error)
  2432  		ErrCheck   func(error) bool
  2433  	}{
  2434  		{
  2435  			Name: "emit issue",
  2436  			Args: func() (tflint.Rule, string, hcl.Range) {
  2437  				return &Rule{}, "this is test", hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}}
  2438  			},
  2439  			ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) {
  2440  				if rule.Name() != "test_rule" {
  2441  					return false, fmt.Errorf("rule.Name() should be test_rule, but %s", rule.Name())
  2442  				}
  2443  				if rule.Enabled() != true {
  2444  					return false, fmt.Errorf("rule.Enabled() should be true, but %t", rule.Enabled())
  2445  				}
  2446  				if rule.Severity() != tflint.ERROR {
  2447  					return false, fmt.Errorf("rule.Severity() should be ERROR, but %s", rule.Severity())
  2448  				}
  2449  				if rule.Link() != "https://example.com" {
  2450  					return false, fmt.Errorf("rule.Link() should be https://example.com, but %s", rule.Link())
  2451  				}
  2452  				if message != "this is test" {
  2453  					return false, fmt.Errorf("message should be `this is test`, but %s", message)
  2454  				}
  2455  				want := hcl.Range{
  2456  					Filename: "test.tf",
  2457  					Start:    hcl.Pos{Line: 2, Column: 2},
  2458  					End:      hcl.Pos{Line: 2, Column: 10},
  2459  				}
  2460  				if diff := cmp.Diff(location, want); diff != "" {
  2461  					return false, fmt.Errorf("diff: %s", diff)
  2462  				}
  2463  				return true, nil
  2464  			},
  2465  			ErrCheck: neverHappend,
  2466  		},
  2467  		{
  2468  			Name: "server returns an error",
  2469  			Args: func() (tflint.Rule, string, hcl.Range) {
  2470  				return &Rule{}, "this is test", hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}}
  2471  			},
  2472  			ServerImpl: func(tflint.Rule, string, hcl.Range, bool) (bool, error) {
  2473  				return false, errors.New("unexpected error")
  2474  			},
  2475  			ErrCheck: func(err error) bool {
  2476  				return err == nil || err.Error() != "unexpected error"
  2477  			},
  2478  		},
  2479  	}
  2480  
  2481  	for _, test := range tests {
  2482  		t.Run(test.Name, func(t *testing.T) {
  2483  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{emitIssue: test.ServerImpl}))
  2484  
  2485  			err := client.EmitIssue(test.Args())
  2486  			if test.ErrCheck(err) {
  2487  				t.Fatalf("failed to call EmitIssue: %s", err)
  2488  			}
  2489  		})
  2490  	}
  2491  }
  2492  
  2493  func TestEmitIssueWithFix(t *testing.T) {
  2494  	// default error check helper
  2495  	neverHappend := func(err error) bool { return err != nil }
  2496  	getFiles := func() map[string][]byte {
  2497  		return map[string][]byte{
  2498  			"test.tf": []byte(`foo = "bar"`),
  2499  		}
  2500  	}
  2501  
  2502  	tests := []struct {
  2503  		Name       string
  2504  		Args       func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error)
  2505  		ServerImpl func(tflint.Rule, string, hcl.Range, bool) (bool, error)
  2506  		ModulePath []string
  2507  		DisableFix bool
  2508  		ErrCheck   func(error) bool
  2509  		Changes    map[string]string
  2510  	}{
  2511  		{
  2512  			Name: "emit issue",
  2513  			Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) {
  2514  				return &Rule{},
  2515  					"this is test",
  2516  					hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}},
  2517  					func(f tflint.Fixer) error {
  2518  						return f.ReplaceText(
  2519  							hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}},
  2520  							"baz",
  2521  						)
  2522  					}
  2523  			},
  2524  			ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) {
  2525  				if rule.Name() != "test_rule" {
  2526  					return false, fmt.Errorf("rule.Name() should be test_rule, but %s", rule.Name())
  2527  				}
  2528  				if rule.Enabled() != true {
  2529  					return false, fmt.Errorf("rule.Enabled() should be true, but %t", rule.Enabled())
  2530  				}
  2531  				if rule.Severity() != tflint.ERROR {
  2532  					return false, fmt.Errorf("rule.Severity() should be ERROR, but %s", rule.Severity())
  2533  				}
  2534  				if rule.Link() != "https://example.com" {
  2535  					return false, fmt.Errorf("rule.Link() should be https://example.com, but %s", rule.Link())
  2536  				}
  2537  				if message != "this is test" {
  2538  					return false, fmt.Errorf("message should be `this is test`, but %s", message)
  2539  				}
  2540  				want := hcl.Range{
  2541  					Filename: "test.tf",
  2542  					Start:    hcl.Pos{Line: 2, Column: 2},
  2543  					End:      hcl.Pos{Line: 2, Column: 10},
  2544  				}
  2545  				if diff := cmp.Diff(location, want); diff != "" {
  2546  					return false, fmt.Errorf("diff: %s", diff)
  2547  				}
  2548  				if fixable != true {
  2549  					return false, fmt.Errorf("fixable should be true, but %t", fixable)
  2550  				}
  2551  				return true, nil
  2552  			},
  2553  			ErrCheck: neverHappend,
  2554  			Changes: map[string]string{
  2555  				"test.tf": `foo = "baz"`,
  2556  			},
  2557  		},
  2558  		{
  2559  			Name: "child modules",
  2560  			Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) {
  2561  				return &Rule{},
  2562  					"this is test",
  2563  					hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}},
  2564  					func(f tflint.Fixer) error {
  2565  						return f.ReplaceText(
  2566  							hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}},
  2567  							"baz",
  2568  						)
  2569  					}
  2570  			},
  2571  			ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) {
  2572  				if fixable != false {
  2573  					return false, fmt.Errorf("fixable should be false, but %t", fixable)
  2574  				}
  2575  				return true, nil
  2576  			},
  2577  			ModulePath: []string{"module", "child"},
  2578  			ErrCheck:   neverHappend,
  2579  			Changes:    map[string]string{},
  2580  		},
  2581  		{
  2582  			Name: "autofix is not supported",
  2583  			Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) {
  2584  				return &Rule{},
  2585  					"this is test",
  2586  					hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}},
  2587  					func(f tflint.Fixer) error {
  2588  						if err := f.ReplaceText(
  2589  							hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}},
  2590  							"baz",
  2591  						); err != nil {
  2592  							return err
  2593  						}
  2594  						return tflint.ErrFixNotSupported
  2595  					}
  2596  			},
  2597  			ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) {
  2598  				if fixable != false {
  2599  					return false, fmt.Errorf("fixable should be false, but %t", fixable)
  2600  				}
  2601  				return true, nil
  2602  			},
  2603  			ErrCheck: neverHappend,
  2604  			Changes:  map[string]string{},
  2605  		},
  2606  		{
  2607  			Name: "fix is disbaled",
  2608  			Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) {
  2609  				return &Rule{},
  2610  					"this is test",
  2611  					hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}},
  2612  					func(f tflint.Fixer) error {
  2613  						return f.ReplaceText(
  2614  							hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}},
  2615  							"baz",
  2616  						)
  2617  					}
  2618  			},
  2619  			ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) {
  2620  				if fixable != true {
  2621  					return false, fmt.Errorf("fixable should be true, but %t", fixable)
  2622  				}
  2623  				return true, nil
  2624  			},
  2625  			DisableFix: true,
  2626  			ErrCheck:   neverHappend,
  2627  			Changes:    map[string]string{},
  2628  		},
  2629  		{
  2630  			Name: "fix is not applied",
  2631  			Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) {
  2632  				return &Rule{},
  2633  					"this is test",
  2634  					hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}},
  2635  					func(f tflint.Fixer) error {
  2636  						return f.ReplaceText(
  2637  							hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}},
  2638  							"baz",
  2639  						)
  2640  					}
  2641  			},
  2642  			ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) {
  2643  				if fixable != true {
  2644  					return false, fmt.Errorf("fixable should be true, but %t", fixable)
  2645  				}
  2646  				return false, nil
  2647  			},
  2648  			ErrCheck: neverHappend,
  2649  			Changes:  map[string]string{},
  2650  		},
  2651  		{
  2652  			Name: "fix raises an error",
  2653  			Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) {
  2654  				return &Rule{},
  2655  					"this is test",
  2656  					hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}},
  2657  					func(f tflint.Fixer) error {
  2658  						if err := f.ReplaceText(
  2659  							hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}},
  2660  							"baz",
  2661  						); err != nil {
  2662  							return err
  2663  						}
  2664  						return errors.New("unexpected error")
  2665  					}
  2666  			},
  2667  			ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) {
  2668  				if fixable != true {
  2669  					return false, fmt.Errorf("fixable should be true, but %t", fixable)
  2670  				}
  2671  				return true, nil
  2672  			},
  2673  			ErrCheck: func(err error) bool {
  2674  				return err == nil || err.Error() != "unexpected error"
  2675  			},
  2676  			Changes: map[string]string{
  2677  				"test.tf": `foo = "baz"`,
  2678  			},
  2679  		},
  2680  		{
  2681  			Name: "server returns an error",
  2682  			Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) {
  2683  				return &Rule{},
  2684  					"this is test",
  2685  					hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}},
  2686  					func(f tflint.Fixer) error {
  2687  						return f.ReplaceText(
  2688  							hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}},
  2689  							"baz",
  2690  						)
  2691  					}
  2692  			},
  2693  			ServerImpl: func(tflint.Rule, string, hcl.Range, bool) (bool, error) {
  2694  				return false, errors.New("unexpected error")
  2695  			},
  2696  			ErrCheck: func(err error) bool {
  2697  				return err == nil || err.Error() != "unexpected error"
  2698  			},
  2699  			Changes: map[string]string{
  2700  				"test.tf": `foo = "baz"`,
  2701  			},
  2702  		},
  2703  	}
  2704  
  2705  	for _, test := range tests {
  2706  		t.Run(test.Name, func(t *testing.T) {
  2707  			client := startTestGRPCServer(
  2708  				t,
  2709  				newMockServer(mockServerImpl{
  2710  					getFiles:      getFiles,
  2711  					getModulePath: func() []string { return test.ModulePath },
  2712  					emitIssue:     test.ServerImpl,
  2713  				}),
  2714  			)
  2715  			client.FixEnabled = !test.DisableFix
  2716  
  2717  			err := client.EmitIssueWithFix(test.Args())
  2718  			if test.ErrCheck(err) {
  2719  				t.Fatalf("failed to call EmitIssueWithFix: %s", err)
  2720  			}
  2721  
  2722  			got := map[string]string{}
  2723  			for name, content := range client.Fixer.Changes() {
  2724  				got[name] = string(content)
  2725  			}
  2726  			if diff := cmp.Diff(got, test.Changes); diff != "" {
  2727  				t.Fatalf("diff: %s", diff)
  2728  			}
  2729  		})
  2730  	}
  2731  }
  2732  
  2733  func TestApplyChanges(t *testing.T) {
  2734  	// default error check helper
  2735  	neverHappend := func(err error) bool { return err != nil }
  2736  	getFiles := func() map[string][]byte {
  2737  		return map[string][]byte{
  2738  			"test.tf": []byte(`foo = "bar"`),
  2739  		}
  2740  	}
  2741  
  2742  	tests := []struct {
  2743  		name       string
  2744  		fix        func(tflint.Fixer) error
  2745  		serverImpl func(map[string][]byte) error
  2746  		errCheck   func(error) bool
  2747  	}{
  2748  		{
  2749  			name: "apply changes",
  2750  			fix: func(f tflint.Fixer) error {
  2751  				return f.ReplaceText(
  2752  					hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}},
  2753  					"baz",
  2754  				)
  2755  			},
  2756  			serverImpl: func(files map[string][]byte) error {
  2757  				if diff := cmp.Diff(files, map[string][]byte{
  2758  					"test.tf": []byte(`foo = "baz"`),
  2759  				}); diff != "" {
  2760  					return fmt.Errorf("diff: %s", diff)
  2761  				}
  2762  				return nil
  2763  			},
  2764  			errCheck: neverHappend,
  2765  		},
  2766  		{
  2767  			name: "server returns an error",
  2768  			fix: func(f tflint.Fixer) error {
  2769  				return f.ReplaceText(
  2770  					hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}},
  2771  					"baz",
  2772  				)
  2773  			},
  2774  			serverImpl: func(files map[string][]byte) error {
  2775  				return errors.New("unexpected error")
  2776  			},
  2777  			errCheck: func(err error) bool {
  2778  				return err == nil || err.Error() != "unexpected error"
  2779  			},
  2780  		},
  2781  	}
  2782  
  2783  	for _, test := range tests {
  2784  		t.Run(test.name, func(t *testing.T) {
  2785  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{getFiles: getFiles, applyChanges: test.serverImpl}))
  2786  			client.FixEnabled = true
  2787  
  2788  			if err := test.fix(client.Fixer); err != nil {
  2789  				t.Fatalf("failed to call fix: %s", err)
  2790  			}
  2791  
  2792  			err := client.ApplyChanges()
  2793  			if test.errCheck(err) {
  2794  				t.Fatalf("failed to call ApplyChanges: %s", err)
  2795  			}
  2796  
  2797  			if err == nil && client.Fixer.HasChanges() {
  2798  				t.Fatal("fixer should have no changes")
  2799  			}
  2800  		})
  2801  	}
  2802  }
  2803  
  2804  func TestEnsureNoError(t *testing.T) {
  2805  	// default error check helper
  2806  	neverHappend := func(err error) bool { return err != nil }
  2807  
  2808  	tests := []struct {
  2809  		Name     string
  2810  		Err      error
  2811  		Proc     func() error
  2812  		ErrCheck func(error) bool
  2813  	}{
  2814  		{
  2815  			Name: "no errors",
  2816  			Err:  nil,
  2817  			Proc: func() error {
  2818  				return errors.New("should be called")
  2819  			},
  2820  			ErrCheck: func(err error) bool {
  2821  				// should be passed result of proc()
  2822  				return err == nil || err.Error() != "should be called"
  2823  			},
  2824  		},
  2825  		{
  2826  			Name: "ErrUnevaluable",
  2827  			Err:  fmt.Errorf("unevaluable%w", tflint.ErrUnevaluable),
  2828  			Proc: func() error {
  2829  				return errors.New("should not be called")
  2830  			},
  2831  			ErrCheck: neverHappend,
  2832  		},
  2833  		{
  2834  			Name: "ErrNullValue",
  2835  			Err:  fmt.Errorf("null value%w", tflint.ErrNullValue),
  2836  			Proc: func() error {
  2837  				return errors.New("should not be called")
  2838  			},
  2839  			ErrCheck: neverHappend,
  2840  		},
  2841  		{
  2842  			Name: "ErrUnknownValue",
  2843  			Err:  fmt.Errorf("unknown value%w", tflint.ErrUnknownValue),
  2844  			Proc: func() error {
  2845  				return errors.New("should not be called")
  2846  			},
  2847  			ErrCheck: neverHappend,
  2848  		},
  2849  		{
  2850  			Name: "unexpected error",
  2851  			Err:  errors.New("unexpected error"),
  2852  			Proc: func() error {
  2853  				return errors.New("should not be called")
  2854  			},
  2855  			ErrCheck: func(err error) bool {
  2856  				return err == nil || err.Error() != "unexpected error"
  2857  			},
  2858  		},
  2859  	}
  2860  
  2861  	for _, test := range tests {
  2862  		t.Run(test.Name, func(t *testing.T) {
  2863  			client := startTestGRPCServer(t, newMockServer(mockServerImpl{}))
  2864  
  2865  			err := client.EnsureNoError(test.Err, test.Proc)
  2866  			if test.ErrCheck(err) {
  2867  				t.Fatalf("failed to call EnsureNoError: %s", err)
  2868  			}
  2869  		})
  2870  	}
  2871  }