github.com/terraform-linters/tflint@v0.51.2-0.20240520175844-3750771571b6/langserver/handler.go (about)

     1  package langserver
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"log"
     8  	"net/url"
     9  	"os"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strings"
    13  
    14  	"github.com/hashicorp/go-version"
    15  	"github.com/hashicorp/hcl/v2"
    16  	lsp "github.com/sourcegraph/go-lsp"
    17  	"github.com/sourcegraph/jsonrpc2"
    18  	"github.com/spf13/afero"
    19  	"github.com/terraform-linters/tflint-plugin-sdk/hclext"
    20  	sdk "github.com/terraform-linters/tflint-plugin-sdk/tflint"
    21  	"github.com/terraform-linters/tflint/plugin"
    22  	"github.com/terraform-linters/tflint/terraform"
    23  	"github.com/terraform-linters/tflint/tflint"
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/grpc/status"
    26  )
    27  
    28  // NewHandler returns a new JSON-RPC handler
    29  func NewHandler(configPath string, cliConfig *tflint.Config) (jsonrpc2.Handler, *plugin.Plugin, error) {
    30  	cfg, err := tflint.LoadConfig(afero.Afero{Fs: afero.NewOsFs()}, configPath)
    31  	if err != nil {
    32  		return nil, nil, err
    33  	}
    34  	if cliConfig.DisabledByDefault {
    35  		for _, rule := range cfg.Rules {
    36  			rule.Enabled = false
    37  		}
    38  	}
    39  	cfg.Merge(cliConfig)
    40  
    41  	rulsetPlugin, err := plugin.Discovery(cfg)
    42  	if err != nil {
    43  		return nil, nil, err
    44  	}
    45  
    46  	rulesets := []tflint.RuleSet{}
    47  	clientSDKVersions := map[string]*version.Version{}
    48  	for name, ruleset := range rulsetPlugin.RuleSets {
    49  		constraints, err := ruleset.VersionConstraints()
    50  		if err != nil {
    51  			if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
    52  				// VersionConstraints endpoint is available in tflint-plugin-sdk v0.14+.
    53  				return nil, nil, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.SDKVersionConstraints)
    54  			} else {
    55  				return nil, nil, fmt.Errorf(`Failed to get TFLint version constraints to "%s" plugin; %w`, name, err)
    56  			}
    57  		}
    58  		if !constraints.Check(tflint.Version) {
    59  			return nil, nil, fmt.Errorf("Failed to satisfy version constraints; tflint-ruleset-%s requires %s, but TFLint version is %s", name, constraints, tflint.Version)
    60  		}
    61  
    62  		clientSDKVersions[name], err = ruleset.SDKVersion()
    63  		if err != nil {
    64  			if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
    65  				// SDKVersion endpoint is available in tflint-plugin-sdk v0.14+.
    66  				return nil, nil, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.SDKVersionConstraints)
    67  			} else {
    68  				return nil, nil, fmt.Errorf(`Failed to get plugin "%s" SDK version; %w`, name, err)
    69  			}
    70  		}
    71  		if !plugin.SDKVersionConstraints.Check(clientSDKVersions[name]) {
    72  			return nil, nil, fmt.Errorf(`Plugin "%s" SDK version (%s) is incompatible. Compatible versions: %s`, name, clientSDKVersions[name], plugin.SDKVersionConstraints)
    73  		}
    74  
    75  		rulesets = append(rulesets, ruleset)
    76  	}
    77  	if err := cliConfig.ValidateRules(rulesets...); err != nil {
    78  		return nil, nil, err
    79  	}
    80  
    81  	return jsonrpc2.HandlerWithError((&handler{
    82  		configPath:        configPath,
    83  		cliConfig:         cliConfig,
    84  		config:            cfg,
    85  		fs:                afero.NewCopyOnWriteFs(afero.NewOsFs(), afero.NewMemMapFs()),
    86  		plugin:            rulsetPlugin,
    87  		clientSDKVersions: clientSDKVersions,
    88  		diagsPaths:        []string{},
    89  	}).handle), rulsetPlugin, nil
    90  }
    91  
    92  type handler struct {
    93  	configPath        string
    94  	cliConfig         *tflint.Config
    95  	config            *tflint.Config
    96  	fs                afero.Fs
    97  	rootDir           string
    98  	plugin            *plugin.Plugin
    99  	clientSDKVersions map[string]*version.Version
   100  	shutdown          bool
   101  	diagsPaths        []string
   102  }
   103  
   104  func (h *handler) handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) {
   105  	if req.Params != nil {
   106  		params, err := json.Marshal(&req.Params)
   107  		if err != nil {
   108  			return nil, &jsonrpc2.Error{
   109  				Code:    jsonrpc2.CodeParseError,
   110  				Message: err.Error(),
   111  				Data:    req.Params,
   112  			}
   113  		}
   114  		log.Printf(`Received %s with %s`, req.Method, string(params))
   115  	} else {
   116  		log.Printf(`Received %s`, req.Method)
   117  	}
   118  
   119  	if h.shutdown && req.Method != "exit" {
   120  		return nil, &jsonrpc2.Error{
   121  			Code:    jsonrpc2.CodeInvalidRequest,
   122  			Message: "server is shutting down",
   123  		}
   124  	}
   125  
   126  	switch req.Method {
   127  	case "initialize":
   128  		return initialize(ctx, conn, req)
   129  	case "initialized":
   130  		return nil, nil
   131  	case "shutdown":
   132  		h.shutdown = true
   133  		return nil, nil
   134  	case "exit":
   135  		return nil, conn.Close()
   136  	case "textDocument/didOpen":
   137  		return h.textDocumentDidOpen(ctx, conn, req)
   138  	case "textDocument/didClose":
   139  		return nil, nil
   140  	case "textDocument/didChange":
   141  		return h.textDocumentDidChange(ctx, conn, req)
   142  	case "workspace/didChangeWatchedFiles":
   143  		return h.workspaceDidChangeWatchedFiles(ctx, conn, req)
   144  	}
   145  
   146  	return nil, &jsonrpc2.Error{
   147  		Code:    jsonrpc2.CodeMethodNotFound,
   148  		Message: fmt.Sprintf("unsupported request: %s", req.Method),
   149  	}
   150  }
   151  
   152  func (h *handler) chdir(dir string) error {
   153  	if h.rootDir != dir {
   154  		log.Printf("Changing directory: %s", dir)
   155  		if err := os.Chdir(dir); err != nil {
   156  			return fmt.Errorf("Failed to chdir to %s: %s", dir, err)
   157  		}
   158  		h.rootDir = dir
   159  	}
   160  	return nil
   161  }
   162  
   163  func (h *handler) inspect() (map[string][]lsp.Diagnostic, error) {
   164  	ret := map[string][]lsp.Diagnostic{}
   165  
   166  	loader, err := terraform.NewLoader(afero.Afero{Fs: h.fs}, h.rootDir)
   167  	if err != nil {
   168  		return ret, fmt.Errorf("Failed to prepare loading: %w", err)
   169  	}
   170  
   171  	configs, diags := loader.LoadConfig(".", h.config.CallModuleType)
   172  	if diags.HasErrors() {
   173  		return ret, fmt.Errorf("Failed to load configurations: %w", diags)
   174  	}
   175  	files, diags := loader.LoadConfigDirFiles(".")
   176  	if diags.HasErrors() {
   177  		return ret, fmt.Errorf("Failed to load configurations: %w", diags)
   178  	}
   179  	annotations := map[string]tflint.Annotations{}
   180  	for path, file := range files {
   181  		if !strings.HasSuffix(path, ".tf") {
   182  			continue
   183  		}
   184  		ants, lexDiags := tflint.NewAnnotations(path, file)
   185  		diags = diags.Extend(lexDiags)
   186  		annotations[path] = ants
   187  	}
   188  
   189  	variables, diags := loader.LoadValuesFiles(".", h.config.Varfiles...)
   190  	if diags.HasErrors() {
   191  		return ret, fmt.Errorf("Failed to load values files: %w", diags)
   192  	}
   193  	cliVars, diags := terraform.ParseVariableValues(h.config.Variables, configs.Module.Variables)
   194  	if diags.HasErrors() {
   195  		return ret, fmt.Errorf("Failed to parse variables: %w", diags)
   196  	}
   197  	variables = append(variables, cliVars)
   198  
   199  	runner, err := tflint.NewRunner(h.rootDir, h.config, annotations, configs, variables...)
   200  	if err != nil {
   201  		return ret, fmt.Errorf("Failed to initialize a runner: %w", err)
   202  	}
   203  	runners, err := tflint.NewModuleRunners(runner)
   204  	if err != nil {
   205  		return ret, fmt.Errorf("Failed to prepare rule checking: %w", err)
   206  	}
   207  	runners = append(runners, runner)
   208  
   209  	config := h.config.ToPluginConfig()
   210  	for name, ruleset := range h.plugin.RuleSets {
   211  		if err := ruleset.ApplyGlobalConfig(config); err != nil {
   212  			return ret, fmt.Errorf(`Failed to apply global config to "%s" plugin`, name)
   213  		}
   214  		configSchema, err := ruleset.ConfigSchema()
   215  		if err != nil {
   216  			return ret, fmt.Errorf(`Failed to fetch config schema from "%s" plugin`, name)
   217  		}
   218  		content := &hclext.BodyContent{}
   219  		if plugin, exists := h.config.Plugins[name]; exists {
   220  			var diags hcl.Diagnostics
   221  			content, diags = plugin.Content(configSchema)
   222  			if diags.HasErrors() {
   223  				return ret, fmt.Errorf(`Failed to parse "%s" plugin config`, name)
   224  			}
   225  		}
   226  		err = ruleset.ApplyConfig(content, h.config.Sources())
   227  		if err != nil {
   228  			return ret, fmt.Errorf(`Failed to apply config to "%s" plugin`, name)
   229  		}
   230  		for _, runner := range runners {
   231  			err = ruleset.Check(plugin.NewGRPCServer(runner, runners[len(runners)-1], loader.Files(), h.clientSDKVersions[name]))
   232  			if err != nil {
   233  				return ret, fmt.Errorf("Failed to check ruleset: %w", err)
   234  			}
   235  		}
   236  	}
   237  
   238  	// In order to publish that the issue has been fixed,
   239  	// notify also the path where the past diagnostics were published.
   240  	for _, path := range h.diagsPaths {
   241  		ret[path] = []lsp.Diagnostic{}
   242  	}
   243  	h.diagsPaths = []string{}
   244  
   245  	for _, runner := range runners {
   246  		for _, issue := range runner.LookupIssues() {
   247  			path := filepath.Join(h.rootDir, issue.Range.Filename)
   248  			h.diagsPaths = append(h.diagsPaths, path)
   249  
   250  			diag := lsp.Diagnostic{
   251  				Message:  issue.Message,
   252  				Severity: toLSPSeverity(issue.Rule.Severity()),
   253  				Range: lsp.Range{
   254  					Start: lsp.Position{Line: issue.Range.Start.Line - 1, Character: issue.Range.Start.Column - 1},
   255  					End:   lsp.Position{Line: issue.Range.End.Line - 1, Character: issue.Range.End.Column - 1},
   256  				},
   257  			}
   258  
   259  			if ret[path] == nil {
   260  				ret[path] = []lsp.Diagnostic{diag}
   261  			} else {
   262  				ret[path] = append(ret[path], diag)
   263  			}
   264  		}
   265  	}
   266  
   267  	return ret, nil
   268  }
   269  
   270  func uriToPath(uri lsp.DocumentURI) (string, error) {
   271  	uriToReplace, err := url.QueryUnescape(string(uri))
   272  	if err != nil {
   273  		return "", err
   274  	}
   275  
   276  	if runtime.GOOS == "windows" {
   277  		return strings.Replace(uriToReplace, "file:///", "", 1), nil
   278  	}
   279  	return strings.Replace(uriToReplace, "file://", "", 1), nil
   280  }
   281  
   282  func pathToURI(path string) lsp.DocumentURI {
   283  	path = filepath.ToSlash(path)
   284  	parts := strings.SplitN(path, "/", 2)
   285  
   286  	head := parts[0]
   287  	if head != "" {
   288  		head = "/" + head
   289  	}
   290  
   291  	rest := ""
   292  	if len(parts) > 1 {
   293  		rest = "/" + parts[1]
   294  	}
   295  
   296  	return lsp.DocumentURI("file://" + head + rest)
   297  }
   298  
   299  func toLSPSeverity(severity tflint.Severity) lsp.DiagnosticSeverity {
   300  	switch severity {
   301  	case sdk.ERROR:
   302  		return lsp.Error
   303  	case sdk.WARNING:
   304  		return lsp.Warning
   305  	case sdk.NOTICE:
   306  		return lsp.Information
   307  	default:
   308  		panic(fmt.Sprintf("Unexpected severity: %s", severity))
   309  	}
   310  }