github.com/tiagovtristao/plz@v13.4.0+incompatible/tools/build_langserver/langserver/diagnostics.go (about) 1 package langserver 2 3 import ( 4 "context" 5 "github.com/thought-machine/please/src/core" 6 "fmt" 7 8 "github.com/thought-machine/please/src/parse/asp" 9 "github.com/thought-machine/please/tools/build_langserver/lsp" 10 11 "github.com/Workiva/go-datastructures/queue" 12 "github.com/sourcegraph/jsonrpc2" 13 ) 14 15 func (h *LsHandler) publishDiagnostics(conn *jsonrpc2.Conn) error { 16 ctx := context.Background() 17 18 // Get task from the queue 19 t, err := h.diagPublisher.queue.Get(1) 20 if err != nil { 21 log.Warning("fail to get diagnostic publishing task") 22 return nil 23 } 24 if len(t) <= 0 { 25 return nil 26 } 27 28 task := t[0].(taskDef) 29 30 // exit if the uri is not in the list of documents 31 if _, ok := h.workspace.documents[task.uri]; !ok { 32 return nil 33 } 34 35 params := &lsp.PublishDiagnosticsParams{ 36 URI: task.uri, 37 Diagnostics: h.diagPublisher.diagnose(ctx, h.analyzer, task.content, task.uri), 38 } 39 40 log.Info("Diagnostics detected: %s", params.Diagnostics) 41 42 return conn.Notify(ctx, "textDocument/publishDiagnostics", params) 43 } 44 45 type diagnosticsPublisher struct { 46 queue *queue.PriorityQueue 47 } 48 49 type diagnosticStore struct { 50 uri lsp.DocumentURI 51 // Subincludes of the file if any 52 subincludes map[string]*RuleDef 53 54 stored []*lsp.Diagnostic 55 } 56 57 type taskDef struct { 58 uri lsp.DocumentURI 59 content string 60 } 61 62 func (td taskDef) Compare(other queue.Item) int { 63 otherTask := other.(taskDef) 64 if otherTask.uri != td.uri || otherTask.content == td.content { 65 return 0 66 } 67 68 return 1 69 } 70 71 func newDiagnosticsPublisher() *diagnosticsPublisher { 72 publisher := &diagnosticsPublisher{ 73 queue: queue.NewPriorityQueue(10000, true), 74 } 75 76 return publisher 77 } 78 79 func (dp *diagnosticsPublisher) diagnose(ctx context.Context, analyzer *Analyzer, content string, uri lsp.DocumentURI) []*lsp.Diagnostic { 80 stmts := analyzer.AspStatementFromContent(content) 81 82 diag := &diagnosticStore{ 83 uri: uri, 84 subincludes: analyzer.GetSubinclude(ctx, stmts, uri), 85 } 86 87 diag.storeDiagnostics(analyzer, stmts) 88 89 return diag.stored 90 } 91 92 func (ds *diagnosticStore) storeDiagnostics(analyzer *Analyzer, stmts []*asp.Statement) { 93 ds.stored = []*lsp.Diagnostic{} 94 95 var callback func(astStruct interface{}) interface{} 96 97 callback = func(astStruct interface{}) interface{} { 98 if stmt, ok := astStruct.(asp.Statement); ok { 99 if stmt.Ident != nil { 100 ds.diagnoseIdentStmt(analyzer, stmt.Ident, stmt.Pos, stmt.EndPos) 101 } 102 } else if expr, ok := astStruct.(asp.Expression); ok { 103 ds.diagnoseExpression(analyzer, expr) 104 } else if identExpr, ok := astStruct.(asp.IdentExpr); ok { 105 ds.diagnoseIdentExpr(analyzer, identExpr, stmts) 106 } 107 108 return nil 109 } 110 111 asp.WalkAST(stmts, callback) 112 } 113 114 func (ds *diagnosticStore) diagnoseIdentStmt(analyzer *Analyzer, ident *asp.IdentStatement, 115 pos asp.Position, endpos asp.Position) { 116 117 if ident.Action != nil && ident.Action.Call != nil { 118 119 funcRange := lsp.Range{ 120 Start: aspPositionToLsp(pos), 121 End: aspPositionToLsp(endpos), 122 } 123 ds.diagnoseFuncCall(analyzer, ident.Name, 124 ident.Action.Call.Arguments, funcRange) 125 } 126 } 127 128 func (ds *diagnosticStore) diagnoseExpression(analyzer *Analyzer, expr asp.Expression) { 129 exprRange := lsp.Range{ 130 Start: aspPositionToLsp(expr.Pos), 131 End: aspPositionToLsp(expr.EndPos), 132 } 133 134 if expr.Val == nil { 135 diag := &lsp.Diagnostic{ 136 Range: exprRange, 137 Severity: lsp.Error, 138 Source: "build", 139 Message: "expression expected", 140 } 141 ds.stored = append(ds.stored, diag) 142 143 } else if expr.Val.String != "" && core.LooksLikeABuildLabel(TrimQuotes(expr.Val.String)) { 144 if diag := ds.diagnosticFromBuildLabel(analyzer, expr.Val.String, exprRange); diag != nil { 145 ds.stored = append(ds.stored, diag) 146 } 147 } 148 } 149 150 func (ds *diagnosticStore) diagnoseIdentExpr(analyzer *Analyzer, identExpr asp.IdentExpr, stmts []*asp.Statement) { 151 152 if identExpr.Action == nil { 153 // Check if variable has been defined 154 pos := aspPositionToLsp(identExpr.Pos) 155 variables := analyzer.VariablesFromStatements(stmts, &pos) 156 157 if _, ok := variables[identExpr.Name]; !ok && !StringInSlice(analyzer.GetConfigNames(), identExpr.Name) { 158 diag := &lsp.Diagnostic{ 159 Range: getNameRange(aspPositionToLsp(identExpr.Pos), identExpr.Name), 160 Severity: lsp.Error, 161 Source: "build", 162 Message: fmt.Sprintf("unexpected variable or config property '%s'", identExpr.Name), 163 } 164 ds.stored = append(ds.stored, diag) 165 } 166 } 167 168 for _, action := range identExpr.Action { 169 if action.Call != nil { 170 identRange := lsp.Range{ 171 Start: aspPositionToLsp(identExpr.Pos), 172 End: aspPositionToLsp(identExpr.EndPos), 173 } 174 ds.diagnoseFuncCall(analyzer, identExpr.Name, action.Call.Arguments, identRange) 175 } 176 } 177 } 178 179 // diagnoseFuncCall checks if the function call's argument name and type are correct 180 // Store a *lsp.diagnostic if found 181 func (ds *diagnosticStore) diagnoseFuncCall(analyzer *Analyzer, funcName string, 182 callArgs []asp.CallArgument, funcRange lsp.Range) { 183 184 // Check if the funcDef is defined 185 def := analyzer.GetBuildRuleByName(funcName, ds.subincludes) 186 if def == nil { 187 diagRange := getNameRange(funcRange.Start, funcName) 188 diag := &lsp.Diagnostic{ 189 Range: diagRange, 190 Severity: lsp.Error, 191 Source: "build", 192 Message: fmt.Sprintf("function undefined: %s", funcName), 193 } 194 ds.stored = append(ds.stored, diag) 195 return 196 } 197 198 for i, arg := range def.Arguments { 199 if def.Object != "" && arg.Name == "self" && i == 0 { 200 continue 201 } 202 203 // Diagnostics for the cases when there are not enough argument passed to the function 204 if len(callArgs)-1 < i { 205 if def.ArgMap[arg.Name].Required == true { 206 diag := &lsp.Diagnostic{ 207 Range: getCallRange(funcRange, funcName), 208 Severity: lsp.Error, 209 Source: "build", 210 Message: fmt.Sprintf("not enough arguments in call to %s", def.Name), 211 } 212 ds.stored = append(ds.stored, diag) 213 break 214 } 215 continue 216 } 217 218 callArg := callArgs[i] 219 argDef := arg 220 221 argRange := lsp.Range{ 222 Start: aspPositionToLsp(callArg.Value.Pos), 223 End: aspPositionToLsp(callArg.Value.EndPos), 224 } 225 // Check if the argument is a valid keyword arg 226 // **Ignore the builtins listed in excludedBuiltins, as the args are not definite 227 if callArg.Name != "" && !StringInSlice(BuiltInsWithIrregularArgs, def.Name) { 228 if _, present := def.ArgMap[callArg.Name]; !present { 229 argRange = getNameRange(aspPositionToLsp(callArg.Pos), callArg.Name) 230 diag := &lsp.Diagnostic{ 231 Range: argRange, 232 Severity: lsp.Error, 233 Source: "build", 234 Message: fmt.Sprintf("unexpected argument %s", callArg.Name), 235 } 236 ds.stored = append(ds.stored, diag) 237 continue 238 } 239 240 // ensure we are checking the correct argument definition 241 // As keyword args can be in any order 242 if callArg.Name != arg.Name { 243 argDef = *def.ArgMap[callArg.Name].Argument 244 } 245 246 } 247 // Check if the argument value type is correct 248 if diag := ds.diagnosticFromCallArgType(analyzer, argDef, callArg); diag != nil { 249 ds.stored = append(ds.stored, diag) 250 } 251 252 } 253 } 254 255 func (ds *diagnosticStore) diagnosticFromCallArgType(analyzer *Analyzer, argDef asp.Argument, callArg asp.CallArgument) *lsp.Diagnostic { 256 argRange := lsp.Range{ 257 Start: aspPositionToLsp(callArg.Value.Pos), 258 End: aspPositionToLsp(callArg.Value.EndPos), 259 } 260 261 // Check if the argument value type is correct 262 var msg string 263 if callArg.Value.Val == nil { 264 msg = "expression expected" 265 } else { 266 var varType string 267 268 if GetValType(callArg.Value.Val) != "" { 269 varType = GetValType(callArg.Value.Val) 270 } else if callArg.Value.Val.Ident != nil { 271 ident := callArg.Value.Val.Ident 272 273 if ident.Action == nil { 274 vars, err := analyzer.VariablesFromURI(ds.uri, &argRange.Start) 275 if err != nil { 276 log.Warning("fail to get variables from %s, skipping", ds.uri) 277 } 278 // We don't have to worry about the case when the variable does not exist, 279 // as it has been taken care of in diagnose 280 if variable, ok := vars[ident.Name]; ok { 281 varType = variable.Type 282 } 283 } else { 284 // Check return types of call if variable is being assigned to a call 285 if retType := ds.getIdentExprReturnType(analyzer, ident); retType != "" { 286 varType = retType 287 } 288 } 289 } 290 291 if varType != "" && len(argDef.Type) != 0 && !StringInSlice(argDef.Type, varType) { 292 msg = fmt.Sprintf("invalid type for argument type '%s' for %s, expecting one of %s", 293 varType, argDef.Name, argDef.Type) 294 } 295 } 296 297 if msg != "" { 298 return &lsp.Diagnostic{ 299 Range: argRange, 300 Severity: lsp.Error, 301 Source: "build", 302 Message: msg, 303 } 304 } 305 return nil 306 } 307 308 func (ds *diagnosticStore) diagnosticFromBuildLabel(analyzer *Analyzer, labelStr string, valRange lsp.Range) *lsp.Diagnostic { 309 trimmed := TrimQuotes(labelStr) 310 311 ctx := context.Background() 312 label, err := analyzer.BuildLabelFromString(ctx, ds.uri, trimmed) 313 if err != nil { 314 return &lsp.Diagnostic{ 315 Range: valRange, 316 Severity: lsp.Error, 317 Source: "build", 318 Message: fmt.Sprintf("Invalid build label %s. error: %s", trimmed, err), 319 } 320 } 321 currentPkg, err := PackageLabelFromURI(ds.uri) 322 323 if label.BuildDef != nil && !isVisible(label.BuildDef, currentPkg) { 324 return &lsp.Diagnostic{ 325 Range: valRange, 326 Severity: lsp.Error, 327 Source: "build", 328 Message: fmt.Sprintf("build label %s is not visible to current package", trimmed), 329 } 330 } 331 return nil 332 } 333 334 func (ds *diagnosticStore) getIdentExprReturnType(analyzer *Analyzer, ident *asp.IdentExpr) string { 335 if ident.Action == nil { 336 return "" 337 } 338 339 for _, action := range ident.Action { 340 if action.Call != nil { 341 if def := analyzer.GetBuildRuleByName(ident.Name, ds.subincludes); def != nil { 342 return def.Return 343 } 344 } else if action.Property != nil { 345 return ds.getIdentExprReturnType(analyzer, action.Property) 346 } 347 } 348 349 return "" 350 } 351 352 /************************ 353 * Helper functions 354 ************************/ 355 func getCallRange(funcRange lsp.Range, funcName string) lsp.Range { 356 return lsp.Range{ 357 Start: lsp.Position{Line: funcRange.Start.Line, 358 Character: funcRange.Start.Character + len(funcName)}, 359 End: funcRange.End, 360 } 361 362 } 363 364 func getNameRange(pos lsp.Position, name string) lsp.Range { 365 return lsp.Range{ 366 Start: pos, 367 End: lsp.Position{Line: pos.Line, 368 Character: pos.Character + len(name)}, 369 } 370 }