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 }