github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/helper/runner_test.go (about)

     1  package helper
     2  
     3  import (
     4  	"errors"
     5  	"testing"
     6  
     7  	"github.com/google/go-cmp/cmp"
     8  	"github.com/google/go-cmp/cmp/cmpopts"
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/hashicorp/hcl/v2/hclsyntax"
    11  	"github.com/terraform-linters/tflint-plugin-sdk/hclext"
    12  	"github.com/terraform-linters/tflint-plugin-sdk/tflint"
    13  	"github.com/zclconf/go-cty/cty"
    14  )
    15  
    16  func Test_GetResourceContent(t *testing.T) {
    17  	cases := []struct {
    18  		Name     string
    19  		Src      string
    20  		Resource string
    21  		Schema   *hclext.BodySchema
    22  		Expected *hclext.BodyContent
    23  	}{
    24  		{
    25  			Name: "attribute",
    26  			Src: `
    27  resource "aws_instance" "foo" {
    28    ami           = "ami-123456"
    29    instance_type = "t2.micro"
    30  }
    31  
    32  resource "aws_s3_bucket" "bar" {
    33    bucket = "my-tf-test-bucket"
    34    acl    = "private"
    35  }`,
    36  			Resource: "aws_instance",
    37  			Schema: &hclext.BodySchema{
    38  				Attributes: []hclext.AttributeSchema{{Name: "instance_type"}},
    39  			},
    40  			Expected: &hclext.BodyContent{
    41  				Blocks: hclext.Blocks{
    42  					{
    43  						Type:   "resource",
    44  						Labels: []string{"aws_instance", "foo"},
    45  						Body: &hclext.BodyContent{
    46  							Attributes: hclext.Attributes{
    47  								"instance_type": {
    48  									Name: "instance_type",
    49  									Expr: &hclsyntax.TemplateExpr{
    50  										Parts: []hclsyntax.Expression{
    51  											&hclsyntax.LiteralValueExpr{
    52  												SrcRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 20}, End: hcl.Pos{Line: 4, Column: 28}},
    53  											},
    54  										},
    55  										SrcRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 19}, End: hcl.Pos{Line: 4, Column: 29}},
    56  									},
    57  									Range:     hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 3}, End: hcl.Pos{Line: 4, Column: 29}},
    58  									NameRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 3}, End: hcl.Pos{Line: 4, Column: 16}},
    59  								},
    60  							},
    61  							Blocks: hclext.Blocks{},
    62  						},
    63  						DefRange:  hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 1}, End: hcl.Pos{Line: 2, Column: 30}},
    64  						TypeRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 1}, End: hcl.Pos{Line: 2, Column: 9}},
    65  						LabelRanges: []hcl.Range{
    66  							{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 10}, End: hcl.Pos{Line: 2, Column: 24}},
    67  							{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 25}, End: hcl.Pos{Line: 2, Column: 30}},
    68  						},
    69  					},
    70  				},
    71  			},
    72  		},
    73  		{
    74  			Name: "block",
    75  			Src: `
    76  resource "aws_instance" "foo" {
    77    ami = "ami-123456"
    78    ebs_block_device {
    79      volume_size = 16
    80    }
    81  }
    82  
    83  resource "aws_s3_bucket" "bar" {
    84    bucket = "my-tf-test-bucket"
    85    acl    = "private"
    86  }`,
    87  			Resource: "aws_instance",
    88  			Schema: &hclext.BodySchema{
    89  				Blocks: []hclext.BlockSchema{
    90  					{Type: "ebs_block_device", Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: "volume_size"}}}},
    91  				},
    92  			},
    93  			Expected: &hclext.BodyContent{
    94  				Blocks: hclext.Blocks{
    95  					{
    96  						Type:   "resource",
    97  						Labels: []string{"aws_instance", "foo"},
    98  						Body: &hclext.BodyContent{
    99  							Attributes: hclext.Attributes{},
   100  							Blocks: hclext.Blocks{
   101  								{
   102  									Type: "ebs_block_device",
   103  									Body: &hclext.BodyContent{
   104  										Attributes: hclext.Attributes{
   105  											"volume_size": {
   106  												Name: "volume_size",
   107  												Expr: &hclsyntax.LiteralValueExpr{
   108  													SrcRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 5, Column: 19}, End: hcl.Pos{Line: 5, Column: 21}},
   109  												},
   110  												Range:     hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 5, Column: 5}, End: hcl.Pos{Line: 5, Column: 21}},
   111  												NameRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 5, Column: 5}, End: hcl.Pos{Line: 5, Column: 16}},
   112  											},
   113  										},
   114  										Blocks: hclext.Blocks{},
   115  									},
   116  									DefRange:  hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 3}, End: hcl.Pos{Line: 4, Column: 19}},
   117  									TypeRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 3}, End: hcl.Pos{Line: 4, Column: 19}},
   118  								},
   119  							},
   120  						},
   121  						DefRange:  hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 1}, End: hcl.Pos{Line: 2, Column: 30}},
   122  						TypeRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 1}, End: hcl.Pos{Line: 2, Column: 9}},
   123  						LabelRanges: []hcl.Range{
   124  							{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 10}, End: hcl.Pos{Line: 2, Column: 24}},
   125  							{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 25}, End: hcl.Pos{Line: 2, Column: 30}},
   126  						},
   127  					},
   128  				},
   129  			},
   130  		},
   131  	}
   132  
   133  	for _, tc := range cases {
   134  		t.Run(tc.Name, func(t *testing.T) {
   135  			runner := TestRunner(t, map[string]string{"main.tf": tc.Src})
   136  
   137  			got, err := runner.GetResourceContent(tc.Resource, tc.Schema, nil)
   138  			if err != nil {
   139  				t.Error(err)
   140  			} else {
   141  				opts := cmp.Options{
   142  					cmpopts.IgnoreFields(hclsyntax.LiteralValueExpr{}, "Val"),
   143  					cmpopts.IgnoreFields(hcl.Pos{}, "Byte"),
   144  				}
   145  				if diff := cmp.Diff(tc.Expected, got, opts...); diff != "" {
   146  					t.Error(diff)
   147  				}
   148  			}
   149  		})
   150  	}
   151  }
   152  
   153  func Test_GetModuleContent(t *testing.T) {
   154  	cases := []struct {
   155  		Name     string
   156  		Src      string
   157  		Schema   *hclext.BodySchema
   158  		Expected *hclext.BodyContent
   159  	}{
   160  		{
   161  			Name: "backend",
   162  			Src: `
   163  terraform {
   164  	backend "s3" {
   165  	bucket = "mybucket"
   166  	key    = "path/to/my/key"
   167  	region = "us-east-1"
   168  	}
   169  }`,
   170  			Schema: &hclext.BodySchema{
   171  				Blocks: []hclext.BlockSchema{
   172  					{
   173  						Type: "terraform",
   174  						Body: &hclext.BodySchema{
   175  							Blocks: []hclext.BlockSchema{
   176  								{
   177  									Type:       "backend",
   178  									LabelNames: []string{"name"},
   179  									Body: &hclext.BodySchema{
   180  										Attributes: []hclext.AttributeSchema{{Name: "bucket"}},
   181  									},
   182  								},
   183  							},
   184  						},
   185  					},
   186  				},
   187  			},
   188  			Expected: &hclext.BodyContent{
   189  				Blocks: hclext.Blocks{
   190  					{
   191  						Type: "terraform",
   192  						Body: &hclext.BodyContent{
   193  							Attributes: hclext.Attributes{},
   194  							Blocks: hclext.Blocks{
   195  								{
   196  									Type:   "backend",
   197  									Labels: []string{"s3"},
   198  									Body: &hclext.BodyContent{
   199  										Attributes: hclext.Attributes{
   200  											"bucket": &hclext.Attribute{
   201  												Name: "bucket",
   202  												Expr: &hclsyntax.TemplateExpr{
   203  													Parts: []hclsyntax.Expression{
   204  														&hclsyntax.LiteralValueExpr{
   205  															SrcRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 12}, End: hcl.Pos{Line: 4, Column: 20}},
   206  														},
   207  													},
   208  													SrcRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 11}, End: hcl.Pos{Line: 4, Column: 21}},
   209  												},
   210  												Range:     hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 2}, End: hcl.Pos{Line: 4, Column: 21}},
   211  												NameRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 2}, End: hcl.Pos{Line: 4, Column: 8}},
   212  											},
   213  										},
   214  										Blocks: hclext.Blocks{},
   215  									},
   216  									DefRange:  hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 2}, End: hcl.Pos{Line: 3, Column: 14}},
   217  									TypeRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 2}, End: hcl.Pos{Line: 3, Column: 9}},
   218  									LabelRanges: []hcl.Range{
   219  										{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 14}},
   220  									},
   221  								},
   222  							},
   223  						},
   224  						DefRange:  hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 1}, End: hcl.Pos{Line: 2, Column: 10}},
   225  						TypeRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 1}, End: hcl.Pos{Line: 2, Column: 10}},
   226  					},
   227  				},
   228  			},
   229  		},
   230  	}
   231  
   232  	for _, tc := range cases {
   233  		t.Run(tc.Name, func(t *testing.T) {
   234  			runner := TestRunner(t, map[string]string{"main.tf": tc.Src})
   235  
   236  			got, err := runner.GetModuleContent(tc.Schema, nil)
   237  			if err != nil {
   238  				t.Error(err)
   239  			} else {
   240  				opts := cmp.Options{
   241  					cmpopts.IgnoreFields(hclsyntax.LiteralValueExpr{}, "Val"),
   242  					cmpopts.IgnoreFields(hcl.Pos{}, "Byte"),
   243  				}
   244  				if diff := cmp.Diff(tc.Expected, got, opts...); diff != "" {
   245  					t.Error(diff)
   246  				}
   247  			}
   248  		})
   249  	}
   250  }
   251  
   252  func Test_GetModuleContent_json(t *testing.T) {
   253  	files := map[string]string{
   254  		"main.tf.json": `{"variable": {"foo": {"type": "string"}}}`,
   255  	}
   256  
   257  	runner := TestRunner(t, files)
   258  
   259  	schema := &hclext.BodySchema{
   260  		Blocks: []hclext.BlockSchema{
   261  			{
   262  				Type: "variable",
   263  				Body: &hclext.BodySchema{
   264  					Blocks: []hclext.BlockSchema{
   265  						{
   266  							Type:       "type",
   267  							LabelNames: []string{"name"},
   268  							Body:       &hclext.BodySchema{},
   269  						},
   270  					},
   271  				},
   272  			},
   273  		},
   274  	}
   275  	got, err := runner.GetModuleContent(schema, nil)
   276  	if err != nil {
   277  		t.Error(err)
   278  	} else {
   279  		if len(got.Blocks) != 1 {
   280  			t.Errorf("got %d blocks, but 1 block is expected", len(got.Blocks))
   281  		}
   282  	}
   283  }
   284  
   285  func TestWalkExpressions(t *testing.T) {
   286  	tests := []struct {
   287  		name   string
   288  		files  map[string]string
   289  		walked []hcl.Range
   290  	}{
   291  		{
   292  			name: "resource",
   293  			files: map[string]string{
   294  				"resource.tf": `
   295  resource "null_resource" "test" {
   296    key = "foo"
   297  }`,
   298  			},
   299  			walked: []hcl.Range{
   300  				{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
   301  				{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
   302  			},
   303  		},
   304  		{
   305  			name: "data source",
   306  			files: map[string]string{
   307  				"data.tf": `
   308  data "null_dataresource" "test" {
   309    key = "foo"
   310  }`,
   311  			},
   312  			walked: []hcl.Range{
   313  				{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
   314  				{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
   315  			},
   316  		},
   317  		{
   318  			name: "module call",
   319  			files: map[string]string{
   320  				"module.tf": `
   321  module "m" {
   322    source = "./module"
   323    key    = "foo"
   324  }`,
   325  			},
   326  			walked: []hcl.Range{
   327  				{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 22}},
   328  				{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 21}},
   329  				{Start: hcl.Pos{Line: 4, Column: 12}, End: hcl.Pos{Line: 4, Column: 17}},
   330  				{Start: hcl.Pos{Line: 4, Column: 13}, End: hcl.Pos{Line: 4, Column: 16}},
   331  			},
   332  		},
   333  		{
   334  			name: "provider config",
   335  			files: map[string]string{
   336  				"provider.tf": `
   337  provider "p" {
   338    key = "foo"
   339  }`,
   340  			},
   341  			walked: []hcl.Range{
   342  				{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
   343  				{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
   344  			},
   345  		},
   346  		{
   347  			name: "locals",
   348  			files: map[string]string{
   349  				"locals.tf": `
   350  locals {
   351    key = "foo"
   352  }`,
   353  			},
   354  			walked: []hcl.Range{
   355  				{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
   356  				{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
   357  			},
   358  		},
   359  		{
   360  			name: "output",
   361  			files: map[string]string{
   362  				"output.tf": `
   363  output "o" {
   364    value = "foo"
   365  }`,
   366  			},
   367  			walked: []hcl.Range{
   368  				{Start: hcl.Pos{Line: 3, Column: 11}, End: hcl.Pos{Line: 3, Column: 16}},
   369  				{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 15}},
   370  			},
   371  		},
   372  		{
   373  			name: "resource with block",
   374  			files: map[string]string{
   375  				"resource.tf": `
   376  resource "null_resource" "test" {
   377    key = "foo"
   378  
   379    lifecycle {
   380      ignore_changes = [key]
   381    }
   382  }`,
   383  			},
   384  			walked: []hcl.Range{
   385  				{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
   386  				{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
   387  				{Start: hcl.Pos{Line: 6, Column: 22}, End: hcl.Pos{Line: 6, Column: 27}},
   388  				{Start: hcl.Pos{Line: 6, Column: 23}, End: hcl.Pos{Line: 6, Column: 26}},
   389  			},
   390  		},
   391  		{
   392  			name: "resource json",
   393  			files: map[string]string{
   394  				"resource.tf.json": `
   395  {
   396    "resource": {
   397      "null_resource": {
   398        "test": {
   399          "key": "foo",
   400          "nested": {
   401            "key": "foo"
   402          },
   403          "list": [{
   404            "key": "foo"
   405          }]
   406        }
   407      }
   408    }
   409  }`,
   410  			},
   411  			walked: []hcl.Range{
   412  				{Start: hcl.Pos{Line: 3, Column: 15}, End: hcl.Pos{Line: 15, Column: 4}},
   413  			},
   414  		},
   415  		{
   416  			name: "multiple files",
   417  			files: map[string]string{
   418  				"main.tf": `
   419  provider "aws" {
   420    region = "us-east-1"
   421  
   422    assume_role {
   423      role_arn = "arn:aws:iam::123412341234:role/ExampleRole"
   424    }
   425  }`,
   426  				"main_override.tf": `
   427  provider "aws" {
   428    region = "us-east-1"
   429  
   430    assume_role {
   431      role_arn = null
   432    }
   433  }`,
   434  			},
   435  			walked: []hcl.Range{
   436  				{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 23}, Filename: "main.tf"},
   437  				{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 22}, Filename: "main.tf"},
   438  				{Start: hcl.Pos{Line: 6, Column: 16}, End: hcl.Pos{Line: 6, Column: 60}, Filename: "main.tf"},
   439  				{Start: hcl.Pos{Line: 6, Column: 17}, End: hcl.Pos{Line: 6, Column: 59}, Filename: "main.tf"},
   440  				{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 23}, Filename: "main_override.tf"},
   441  				{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 22}, Filename: "main_override.tf"},
   442  				{Start: hcl.Pos{Line: 6, Column: 16}, End: hcl.Pos{Line: 6, Column: 20}, Filename: "main_override.tf"},
   443  			},
   444  		},
   445  		{
   446  			name: "nested attributes",
   447  			files: map[string]string{
   448  				"data.tf": `
   449  data "terraform_remote_state" "remote_state" {
   450    backend = "remote"
   451  
   452    config = {
   453      organization = "Organization"
   454      workspaces = {
   455        name = "${var.environment}"
   456      }
   457    }
   458  }`,
   459  			},
   460  			walked: []hcl.Range{
   461  				{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 21}},
   462  				{Start: hcl.Pos{Line: 3, Column: 14}, End: hcl.Pos{Line: 3, Column: 20}},
   463  				{Start: hcl.Pos{Line: 5, Column: 12}, End: hcl.Pos{Line: 10, Column: 4}},
   464  				{Start: hcl.Pos{Line: 6, Column: 5}, End: hcl.Pos{Line: 6, Column: 17}},
   465  				{Start: hcl.Pos{Line: 6, Column: 20}, End: hcl.Pos{Line: 6, Column: 34}},
   466  				{Start: hcl.Pos{Line: 6, Column: 21}, End: hcl.Pos{Line: 6, Column: 33}},
   467  				{Start: hcl.Pos{Line: 7, Column: 5}, End: hcl.Pos{Line: 7, Column: 15}},
   468  				{Start: hcl.Pos{Line: 7, Column: 18}, End: hcl.Pos{Line: 9, Column: 6}},
   469  				{Start: hcl.Pos{Line: 8, Column: 7}, End: hcl.Pos{Line: 8, Column: 11}},
   470  				{Start: hcl.Pos{Line: 8, Column: 14}, End: hcl.Pos{Line: 8, Column: 34}},
   471  				{Start: hcl.Pos{Line: 8, Column: 17}, End: hcl.Pos{Line: 8, Column: 32}},
   472  			},
   473  		},
   474  	}
   475  
   476  	for _, test := range tests {
   477  		t.Run(test.name, func(t *testing.T) {
   478  			runner := TestRunner(t, test.files)
   479  
   480  			walked := []hcl.Range{}
   481  			diags := runner.WalkExpressions(tflint.ExprWalkFunc(func(expr hcl.Expression) hcl.Diagnostics {
   482  				walked = append(walked, expr.Range())
   483  				return nil
   484  			}))
   485  			if diags.HasErrors() {
   486  				t.Fatal(diags)
   487  			}
   488  			opts := cmp.Options{
   489  				cmpopts.IgnoreFields(hcl.Range{}, "Filename"),
   490  				cmpopts.IgnoreFields(hcl.Pos{}, "Byte"),
   491  				cmpopts.SortSlices(func(x, y hcl.Range) bool { return x.String() > y.String() }),
   492  			}
   493  			if diff := cmp.Diff(walked, test.walked, opts); diff != "" {
   494  				t.Error(diff)
   495  			}
   496  		})
   497  	}
   498  }
   499  
   500  func Test_DecodeRuleConfig(t *testing.T) {
   501  	files := map[string]string{
   502  		".tflint.hcl": `
   503  rule "test" {
   504    enabled = true
   505    foo     = "bar"
   506  }`,
   507  	}
   508  
   509  	runner := TestRunner(t, files)
   510  
   511  	type ruleConfig struct {
   512  		Foo string `hclext:"foo"`
   513  	}
   514  	target := &ruleConfig{}
   515  	if err := runner.DecodeRuleConfig("test", target); err != nil {
   516  		t.Fatal(err)
   517  	}
   518  
   519  	if target.Foo != "bar" {
   520  		t.Errorf("target.Foo should be `bar`, but got `%s`", target.Foo)
   521  	}
   522  }
   523  
   524  func Test_DecodeRuleConfig_config_not_found(t *testing.T) {
   525  	runner := TestRunner(t, map[string]string{})
   526  
   527  	type ruleConfig struct {
   528  		Foo string `hclext:"foo"`
   529  	}
   530  	target := &ruleConfig{}
   531  	if err := runner.DecodeRuleConfig("test", target); err != nil {
   532  		t.Fatal(err)
   533  	}
   534  
   535  	if target.Foo != "" {
   536  		t.Errorf("target.Foo should be empty, but got `%s`", target.Foo)
   537  	}
   538  }
   539  
   540  func Test_EvaluateExpr_string(t *testing.T) {
   541  	tests := []struct {
   542  		Name string
   543  		Src  string
   544  		Want string
   545  	}{
   546  		{
   547  			Name: "string literal",
   548  			Src: `
   549  resource "aws_instance" "foo" {
   550    instance_type = "t2.micro"
   551  }`,
   552  			Want: "t2.micro",
   553  		},
   554  		{
   555  			Name: "string interpolation",
   556  			Src: `
   557  variable "instance_type" {
   558  	type = string
   559    default = "t2.micro"
   560  }
   561  
   562  resource "aws_instance" "foo" {
   563    instance_type = var.instance_type
   564  }`,
   565  			Want: "t2.micro",
   566  		},
   567  	}
   568  
   569  	for _, test := range tests {
   570  		t.Run(test.Name, func(t *testing.T) {
   571  			runner := TestRunner(t, map[string]string{"main.tf": test.Src})
   572  
   573  			resources, err := runner.GetResourceContent("aws_instance", &hclext.BodySchema{
   574  				Attributes: []hclext.AttributeSchema{{Name: "instance_type"}},
   575  			}, nil)
   576  			if err != nil {
   577  				t.Fatal(err)
   578  			}
   579  
   580  			for _, resource := range resources.Blocks {
   581  				// raw value
   582  				var instanceType string
   583  				if err := runner.EvaluateExpr(resource.Body.Attributes["instance_type"].Expr, &instanceType, nil); err != nil {
   584  					t.Fatal(err)
   585  				}
   586  
   587  				if instanceType != test.Want {
   588  					t.Fatalf(`"%s" is expected, but got "%s"`, test.Want, instanceType)
   589  				}
   590  
   591  				// callback
   592  				if err := runner.EvaluateExpr(resource.Body.Attributes["instance_type"].Expr, func(val string) error {
   593  					if instanceType != test.Want {
   594  						t.Fatalf(`"%s" is expected, but got "%s"`, test.Want, instanceType)
   595  					}
   596  					return nil
   597  				}, nil); err != nil {
   598  					t.Fatal(err)
   599  				}
   600  			}
   601  		})
   602  	}
   603  }
   604  
   605  func Test_EvaluateExpr_value(t *testing.T) {
   606  	tests := []struct {
   607  		Name string
   608  		Src  string
   609  		Want string
   610  	}{
   611  		{
   612  			Name: "sensitive variable",
   613  			Src: `
   614  variable "instance_type" {
   615    type = string
   616    default = "secret"
   617    sensitive = true
   618  }
   619  
   620  resource "aws_instance" "foo" {
   621    instance_type = var.instance_type
   622  }`,
   623  			Want: `cty.StringVal("secret").Mark(marks.Sensitive)`,
   624  		},
   625  		{
   626  			Name: "ephemeral variable",
   627  			Src: `
   628  variable "instance_type" {
   629    type = string
   630    default = "secret"
   631    ephemeral = true
   632  }
   633  
   634  resource "aws_instance" "foo" {
   635    instance_type = var.instance_type
   636  }`,
   637  			Want: `cty.StringVal("secret").Mark(marks.Ephemeral)`,
   638  		},
   639  	}
   640  
   641  	for _, test := range tests {
   642  		t.Run(test.Name, func(t *testing.T) {
   643  			runner := TestRunner(t, map[string]string{"main.tf": test.Src})
   644  
   645  			resources, err := runner.GetResourceContent("aws_instance", &hclext.BodySchema{
   646  				Attributes: []hclext.AttributeSchema{{Name: "instance_type"}},
   647  			}, nil)
   648  			if err != nil {
   649  				t.Fatal(err)
   650  			}
   651  
   652  			for _, resource := range resources.Blocks {
   653  				// raw value
   654  				var instanceType cty.Value
   655  				if err := runner.EvaluateExpr(resource.Body.Attributes["instance_type"].Expr, &instanceType, nil); err != nil {
   656  					t.Fatal(err)
   657  				}
   658  
   659  				if instanceType.GoString() != test.Want {
   660  					t.Fatalf(`"%s" is expected, but got "%s"`, test.Want, instanceType.GoString())
   661  				}
   662  
   663  				// callback
   664  				if err := runner.EvaluateExpr(resource.Body.Attributes["instance_type"].Expr, func(val cty.Value) error {
   665  					if instanceType.GoString() != test.Want {
   666  						t.Fatalf(`"%s" is expected, but got "%s"`, test.Want, instanceType.GoString())
   667  					}
   668  					return nil
   669  				}, nil); err != nil {
   670  					t.Fatal(err)
   671  				}
   672  			}
   673  		})
   674  	}
   675  }
   676  
   677  type dummyRule struct {
   678  	tflint.DefaultRule
   679  }
   680  
   681  func (r *dummyRule) Name() string              { return "dummy_rule" }
   682  func (r *dummyRule) Enabled() bool             { return true }
   683  func (r *dummyRule) Severity() tflint.Severity { return tflint.ERROR }
   684  func (r *dummyRule) Check(tflint.Runner) error { return nil }
   685  
   686  func Test_EmitIssue(t *testing.T) {
   687  	src := `
   688  resource "aws_instance" "foo" {
   689    instance_type = "t2.micro"
   690  }`
   691  
   692  	runner := TestRunner(t, map[string]string{"main.tf": src})
   693  
   694  	resources, err := runner.GetResourceContent("aws_instance", &hclext.BodySchema{
   695  		Attributes: []hclext.AttributeSchema{{Name: "instance_type"}},
   696  	}, nil)
   697  	if err != nil {
   698  		t.Fatal(err)
   699  	}
   700  
   701  	for _, resource := range resources.Blocks {
   702  		if err := runner.EmitIssue(&dummyRule{}, "issue found", resource.Body.Attributes["instance_type"].Expr.Range()); err != nil {
   703  			t.Fatal(err)
   704  		}
   705  	}
   706  
   707  	expected := Issues{
   708  		{
   709  			Rule:    &dummyRule{},
   710  			Message: "issue found",
   711  			Range:   hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 19}, End: hcl.Pos{Line: 3, Column: 29}},
   712  		},
   713  	}
   714  
   715  	opt := cmpopts.IgnoreFields(hcl.Pos{}, "Byte")
   716  	if diff := cmp.Diff(expected, runner.Issues, opt); diff != "" {
   717  		t.Fatal(diff)
   718  	}
   719  }
   720  
   721  func Test_EmitIssueWithFix(t *testing.T) {
   722  	// default error check helper
   723  	neverHappend := func(err error) bool { return err != nil }
   724  
   725  	tests := []struct {
   726  		name     string
   727  		src      string
   728  		rng      hcl.Range
   729  		fix      func(tflint.Fixer) error
   730  		want     Issues
   731  		fixed    string
   732  		errCheck func(error) bool
   733  	}{
   734  		{
   735  			name: "with fix",
   736  			src: `
   737  resource "aws_instance" "foo" {
   738    instance_type = "t2.micro"
   739  }`,
   740  			rng: hcl.Range{
   741  				Filename: "main.tf",
   742  				Start:    hcl.Pos{Line: 3, Column: 19, Byte: 51},
   743  				End:      hcl.Pos{Line: 3, Column: 29, Byte: 61},
   744  			},
   745  			fix: func(fixer tflint.Fixer) error {
   746  				return fixer.ReplaceText(
   747  					hcl.Range{
   748  						Filename: "main.tf",
   749  						Start:    hcl.Pos{Line: 3, Column: 19, Byte: 51},
   750  						End:      hcl.Pos{Line: 3, Column: 29, Byte: 61},
   751  					},
   752  					`"t3.micro"`,
   753  				)
   754  			},
   755  			want: Issues{
   756  				{
   757  					Rule:    &dummyRule{},
   758  					Message: "issue found",
   759  					Range:   hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 19}, End: hcl.Pos{Line: 3, Column: 29}},
   760  				},
   761  			},
   762  			fixed: `
   763  resource "aws_instance" "foo" {
   764    instance_type = "t3.micro"
   765  }`,
   766  			errCheck: neverHappend,
   767  		},
   768  		{
   769  			name: "autofix is not supported",
   770  			src: `
   771  resource "aws_instance" "foo" {
   772    instance_type = "t2.micro"
   773  }`,
   774  			rng: hcl.Range{
   775  				Filename: "main.tf",
   776  				Start:    hcl.Pos{Line: 3, Column: 19, Byte: 51},
   777  				End:      hcl.Pos{Line: 3, Column: 29, Byte: 61},
   778  			},
   779  			fix: func(fixer tflint.Fixer) error {
   780  				if err := fixer.ReplaceText(
   781  					hcl.Range{
   782  						Filename: "main.tf",
   783  						Start:    hcl.Pos{Line: 3, Column: 19, Byte: 51},
   784  						End:      hcl.Pos{Line: 3, Column: 29, Byte: 61},
   785  					},
   786  					`"t3.micro"`,
   787  				); err != nil {
   788  					return err
   789  				}
   790  				return tflint.ErrFixNotSupported
   791  			},
   792  			want: Issues{
   793  				{
   794  					Rule:    &dummyRule{},
   795  					Message: "issue found",
   796  					Range:   hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 19}, End: hcl.Pos{Line: 3, Column: 29}},
   797  				},
   798  			},
   799  			errCheck: neverHappend,
   800  		},
   801  		{
   802  			name: "other errors",
   803  			src: `
   804  resource "aws_instance" "foo" {
   805    instance_type = "t2.micro"
   806  }`,
   807  			rng: hcl.Range{
   808  				Filename: "main.tf",
   809  				Start:    hcl.Pos{Line: 3, Column: 19, Byte: 51},
   810  				End:      hcl.Pos{Line: 3, Column: 29, Byte: 61},
   811  			},
   812  			fix: func(fixer tflint.Fixer) error {
   813  				if err := fixer.ReplaceText(
   814  					hcl.Range{
   815  						Filename: "main.tf",
   816  						Start:    hcl.Pos{Line: 3, Column: 19, Byte: 51},
   817  						End:      hcl.Pos{Line: 3, Column: 29, Byte: 61},
   818  					},
   819  					`"t3.micro"`,
   820  				); err != nil {
   821  					return err
   822  				}
   823  				return errors.New("unexpected error")
   824  			},
   825  			want: Issues{},
   826  			fixed: `
   827  resource "aws_instance" "foo" {
   828    instance_type = "t3.micro"
   829  }`,
   830  			errCheck: func(err error) bool {
   831  				return err == nil && err.Error() != "unexpected error"
   832  			},
   833  		},
   834  	}
   835  
   836  	for _, test := range tests {
   837  		t.Run(test.name, func(t *testing.T) {
   838  			runner := TestRunner(t, map[string]string{"main.tf": test.src})
   839  
   840  			err := runner.EmitIssueWithFix(&dummyRule{}, "issue found", test.rng, test.fix)
   841  			if test.errCheck(err) {
   842  				t.Fatal(err)
   843  			}
   844  
   845  			opt := cmpopts.IgnoreFields(hcl.Pos{}, "Byte")
   846  			if diff := cmp.Diff(test.want, runner.Issues, opt); diff != "" {
   847  				t.Fatal(diff)
   848  			}
   849  			if diff := cmp.Diff(test.fixed, string(runner.Changes()["main.tf"]), opt); diff != "" {
   850  				t.Fatal(diff)
   851  			}
   852  		})
   853  	}
   854  }
   855  
   856  func TestChanges(t *testing.T) {
   857  	tests := []struct {
   858  		name string
   859  		src  string
   860  		fix  func(tflint.Fixer) error
   861  		want string
   862  	}{
   863  		{
   864  			name: "changes",
   865  			src: `
   866  locals {
   867    foo = "bar"
   868  }`,
   869  			fix: func(fixer tflint.Fixer) error {
   870  				return fixer.InsertTextBefore(
   871  					hcl.Range{
   872  						Filename: "main.tf",
   873  						Start:    hcl.Pos{Byte: 12},
   874  						End:      hcl.Pos{Byte: 15},
   875  					},
   876  					"bar = \"baz\"\n",
   877  				)
   878  			},
   879  			want: `
   880  locals {
   881    bar = "baz"
   882    foo = "bar"
   883  }`,
   884  		},
   885  	}
   886  
   887  	for _, test := range tests {
   888  		t.Run(test.name, func(t *testing.T) {
   889  			runner := TestRunner(t, map[string]string{"main.tf": test.src})
   890  
   891  			if err := test.fix(runner.fixer); err != nil {
   892  				t.Fatal(err)
   893  			}
   894  
   895  			if diff := cmp.Diff(test.want, string(runner.Changes()["main.tf"])); diff != "" {
   896  				t.Fatal(diff)
   897  			}
   898  		})
   899  	}
   900  }
   901  
   902  func Test_EnsureNoError(t *testing.T) {
   903  	runner := TestRunner(t, map[string]string{})
   904  
   905  	var run bool
   906  	err := runner.EnsureNoError(nil, func() error {
   907  		run = true
   908  		return nil
   909  	})
   910  	if err != nil {
   911  		t.Fatal(err)
   912  	}
   913  
   914  	if !run {
   915  		t.Fatal("Expected to exec the passed proc, but doesn't")
   916  	}
   917  }