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  }